prettier-plugin-wolfram 0.7.0
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/LICENSE +7 -0
- package/README.md +290 -0
- package/bin/prettier-wolfram.js +55 -0
- package/package.json +58 -0
- package/src/index.js +80 -0
- package/src/options.js +206 -0
- package/src/parser/adapter.js +690 -0
- package/src/parser/cstEqual.js +18 -0
- package/src/parser/index.js +29 -0
- package/src/parser/operators.js +35 -0
- package/src/parser/position.js +62 -0
- package/src/parser/tree-sitter-wolfram.wasm +0 -0
- package/src/range.js +98 -0
- package/src/rules/index.js +57 -0
- package/src/rules/line-width.js +129 -0
- package/src/rules/newlines-between-definitions.js +103 -0
- package/src/rules/no-bare-symbol-set.js +19 -0
- package/src/rules/no-dynamic-module-leak.js +74 -0
- package/src/rules/no-general-infix-function.js +52 -0
- package/src/rules/no-shadowed-pattern.js +71 -0
- package/src/rules/no-unused-module-var.js +84 -0
- package/src/rules/prefer-rule-delayed.js +59 -0
- package/src/rules/spacing-commas.js +64 -0
- package/src/rules/spacing-operators.js +87 -0
- package/src/translator/commentSpacing.js +51 -0
- package/src/translator/docComments.js +89 -0
- package/src/translator/index.js +98 -0
- package/src/translator/nodes/binary.js +205 -0
- package/src/translator/nodes/call.js +254 -0
- package/src/translator/nodes/compound.js +117 -0
- package/src/translator/nodes/container.js +194 -0
- package/src/translator/nodes/group.js +159 -0
- package/src/translator/nodes/infix.js +408 -0
- package/src/translator/nodes/leaf.js +605 -0
- package/src/translator/nodes/postfix.js +29 -0
- package/src/translator/nodes/prefix.js +27 -0
- package/src/translator/nodes/ternary.js +82 -0
- package/src/translator/ruleAlignment.js +133 -0
- package/src/translator/sourceLines.js +49 -0
- package/src/translator/sourcePreservation.js +22 -0
- package/src/translator/specialForms.js +665 -0
- package/src/utils/codeSpacing.js +420 -0
- package/src/utils/cstErrors.js +36 -0
- package/src/utils/offsets.js +132 -0
- package/src/utils/operatorSpacing.js +49 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/rules/no-shadowed-pattern.js
|
|
2
|
+
|
|
3
|
+
function collectModuleVars(node) {
|
|
4
|
+
const vars = new Set();
|
|
5
|
+
if (node.type !== "CallNode") return vars;
|
|
6
|
+
const headName = node.head?.value;
|
|
7
|
+
if (!["Module", "With", "Block", "DynamicModule"].includes(headName))
|
|
8
|
+
return vars;
|
|
9
|
+
const varList = node.children?.find((c) => c.type === "GroupNode");
|
|
10
|
+
if (!varList) return vars;
|
|
11
|
+
varList.children?.forEach((c) => {
|
|
12
|
+
if (c.type === "LeafNode" && c.kind === "Symbol") vars.add(c.value);
|
|
13
|
+
if (c.type === "BinaryNode" && c.op === "Set") {
|
|
14
|
+
const lhs = c.children?.[0];
|
|
15
|
+
if (lhs?.type === "LeafNode") vars.add(lhs.value);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return vars;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findPatternVars(node) {
|
|
22
|
+
const vars = [];
|
|
23
|
+
walk(node, (n) => {
|
|
24
|
+
if (n.type === "CompoundNode" && n.op?.startsWith("Pattern")) {
|
|
25
|
+
const name = n.children?.find(
|
|
26
|
+
(c) => c.type === "LeafNode" && c.kind === "Symbol",
|
|
27
|
+
);
|
|
28
|
+
if (name) vars.push({ name: name.value, node: n });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return vars;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function walk(node, fn) {
|
|
35
|
+
if (!node) return;
|
|
36
|
+
fn(node);
|
|
37
|
+
node.children?.forEach((c) => walk(c, fn));
|
|
38
|
+
if (node.head) walk(node.head, fn);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
name: "no-shadowed-pattern",
|
|
43
|
+
description: "Pattern variable shadows an outer Module/With/Block variable",
|
|
44
|
+
defaultLevel: "error",
|
|
45
|
+
|
|
46
|
+
visit(node, context) {
|
|
47
|
+
if (
|
|
48
|
+
node.type !== "BinaryNode" ||
|
|
49
|
+
!["Set", "SetDelayed"].includes(node.op)
|
|
50
|
+
)
|
|
51
|
+
return;
|
|
52
|
+
|
|
53
|
+
const rhs = node.children?.[node.children.length - 1];
|
|
54
|
+
if (!rhs) return;
|
|
55
|
+
|
|
56
|
+
const moduleVars = collectModuleVars(rhs);
|
|
57
|
+
if (moduleVars.size === 0) return;
|
|
58
|
+
|
|
59
|
+
const lhs = node.children?.[0];
|
|
60
|
+
const patVars = findPatternVars(lhs);
|
|
61
|
+
|
|
62
|
+
for (const { name, node: patNode } of patVars) {
|
|
63
|
+
if (moduleVars.has(name)) {
|
|
64
|
+
context.report({
|
|
65
|
+
node: patNode,
|
|
66
|
+
message: `Pattern variable "${name}" shadows a Module/With/Block local variable.`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/rules/no-unused-module-var.js
|
|
2
|
+
|
|
3
|
+
function getModuleVarNames(varListNode) {
|
|
4
|
+
const names = [];
|
|
5
|
+
if (!varListNode?.children) return names;
|
|
6
|
+
for (const child of varListNode.children) {
|
|
7
|
+
if (child.type === "LeafNode" && child.kind === "Symbol") {
|
|
8
|
+
names.push(child.value);
|
|
9
|
+
} else if (child.type === "BinaryNode" && child.op === "Set") {
|
|
10
|
+
const lhs = child.children?.[0];
|
|
11
|
+
if (lhs?.type === "LeafNode" && lhs.kind === "Symbol")
|
|
12
|
+
names.push(lhs.value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return names;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function symbolsUsed(node, exclude = new Set()) {
|
|
19
|
+
const used = new Set();
|
|
20
|
+
walk(node, (n) => {
|
|
21
|
+
if (
|
|
22
|
+
n.type === "LeafNode" &&
|
|
23
|
+
n.kind === "Symbol" &&
|
|
24
|
+
!exclude.has(n.value)
|
|
25
|
+
) {
|
|
26
|
+
used.add(n.value);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return used;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walk(node, fn) {
|
|
33
|
+
if (!node) return;
|
|
34
|
+
fn(node);
|
|
35
|
+
node.children?.forEach((c) => walk(c, fn));
|
|
36
|
+
if (node.head) walk(node.head, fn);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const BLOCK_OPS = new Set(["Module", "With", "Block", "DynamicModule"]);
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
name: "no-unused-module-var",
|
|
43
|
+
description:
|
|
44
|
+
"Variables declared in Module/With/Block but never used in the body",
|
|
45
|
+
defaultLevel: "warn",
|
|
46
|
+
|
|
47
|
+
visit(node, context) {
|
|
48
|
+
if (node.type !== "CallNode") return;
|
|
49
|
+
const headName = node.head?.value;
|
|
50
|
+
if (!BLOCK_OPS.has(headName)) return;
|
|
51
|
+
|
|
52
|
+
const args =
|
|
53
|
+
node.children?.filter(
|
|
54
|
+
(c) =>
|
|
55
|
+
!(
|
|
56
|
+
c.type === "LeafNode" &&
|
|
57
|
+
[
|
|
58
|
+
"Token`Comma",
|
|
59
|
+
"Token`Whitespace",
|
|
60
|
+
"Token`Newline",
|
|
61
|
+
].includes(c.kind)
|
|
62
|
+
),
|
|
63
|
+
) ?? [];
|
|
64
|
+
|
|
65
|
+
if (args.length < 2) return;
|
|
66
|
+
const varList = args[0];
|
|
67
|
+
const body = args.slice(1);
|
|
68
|
+
|
|
69
|
+
const declared = getModuleVarNames(varList);
|
|
70
|
+
if (declared.length === 0) return;
|
|
71
|
+
|
|
72
|
+
const bodyUsed = new Set();
|
|
73
|
+
body.forEach((b) => symbolsUsed(b).forEach((s) => bodyUsed.add(s)));
|
|
74
|
+
|
|
75
|
+
for (const name of declared) {
|
|
76
|
+
if (!bodyUsed.has(name)) {
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
message: `Variable "${name}" declared in ${headName}[{...}] is never used in the body.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/rules/prefer-rule-delayed.js
|
|
2
|
+
|
|
3
|
+
function collectPatternVars(node, vars = new Set()) {
|
|
4
|
+
if (!node) return vars;
|
|
5
|
+
if (
|
|
6
|
+
node.type === "CompoundNode" &&
|
|
7
|
+
(node.op === "PatternBlank" ||
|
|
8
|
+
node.op === "PatternBlankSequence" ||
|
|
9
|
+
node.op === "PatternBlankNullSequence" ||
|
|
10
|
+
node.op === "Pattern")
|
|
11
|
+
) {
|
|
12
|
+
const namePart = node.children?.find(
|
|
13
|
+
(c) => c.type === "LeafNode" && c.kind === "Symbol",
|
|
14
|
+
);
|
|
15
|
+
if (namePart) vars.add(namePart.value);
|
|
16
|
+
}
|
|
17
|
+
node.children?.forEach((c) => collectPatternVars(c, vars));
|
|
18
|
+
if (node.head) collectPatternVars(node.head, vars);
|
|
19
|
+
return vars;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function containsSymbol(node, names) {
|
|
23
|
+
if (!node) return false;
|
|
24
|
+
if (
|
|
25
|
+
node.type === "LeafNode" &&
|
|
26
|
+
node.kind === "Symbol" &&
|
|
27
|
+
names.has(node.value)
|
|
28
|
+
)
|
|
29
|
+
return true;
|
|
30
|
+
if (node.children?.some((c) => containsSymbol(c, names))) return true;
|
|
31
|
+
if (node.head && containsSymbol(node.head, names)) return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
name: "prefer-rule-delayed",
|
|
37
|
+
description:
|
|
38
|
+
"f[x_] = body should be := when body references pattern variables",
|
|
39
|
+
defaultLevel: "warn",
|
|
40
|
+
|
|
41
|
+
visit(node, context) {
|
|
42
|
+
if (node.type !== "BinaryNode" || node.op !== "Set") return;
|
|
43
|
+
const [lhs, rhs] = [
|
|
44
|
+
node.children?.[0],
|
|
45
|
+
node.children?.[node.children.length - 1],
|
|
46
|
+
];
|
|
47
|
+
if (!lhs || !rhs) return;
|
|
48
|
+
|
|
49
|
+
const patVars = collectPatternVars(lhs);
|
|
50
|
+
if (patVars.size === 0) return;
|
|
51
|
+
|
|
52
|
+
if (containsSymbol(rhs, patVars)) {
|
|
53
|
+
context.report({
|
|
54
|
+
node,
|
|
55
|
+
message: `Use SetDelayed (:=) instead of Set (=) when the right-hand side references pattern variables (${[...patVars].join(", ")}).`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/rules/spacing-commas.js
|
|
2
|
+
|
|
3
|
+
function isWhitespace(node) {
|
|
4
|
+
return (
|
|
5
|
+
node?.type === "LeafNode" &&
|
|
6
|
+
["Token`Whitespace", "Whitespace"].includes(node.kind)
|
|
7
|
+
);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isNewline(node) {
|
|
11
|
+
return (
|
|
12
|
+
node?.type === "LeafNode" &&
|
|
13
|
+
["Token`Newline", "Newline"].includes(node.kind)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isComma(node) {
|
|
18
|
+
return node?.type === "LeafNode" && node.kind === "Token`Comma";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
name: "spacing-commas",
|
|
23
|
+
description:
|
|
24
|
+
"Comma spacing inconsistent with wolfram.spaceAfterComma option",
|
|
25
|
+
defaultLevel: "warn",
|
|
26
|
+
fixableByFormatter: true,
|
|
27
|
+
|
|
28
|
+
visit(node, context) {
|
|
29
|
+
const children = node.children ?? [];
|
|
30
|
+
const wantSpaceAfter = context.options?.wolframSpaceAfterComma ?? true;
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < children.length; i++) {
|
|
33
|
+
const child = children[i];
|
|
34
|
+
if (!isComma(child)) continue;
|
|
35
|
+
|
|
36
|
+
const prev = children[i - 1];
|
|
37
|
+
const next = children[i + 1];
|
|
38
|
+
|
|
39
|
+
if (isWhitespace(prev)) {
|
|
40
|
+
context.report({
|
|
41
|
+
node: prev,
|
|
42
|
+
message: "Unexpected space before comma.",
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (wantSpaceAfter) {
|
|
48
|
+
if (!isWhitespace(next) && !isNewline(next) && next) {
|
|
49
|
+
context.report({
|
|
50
|
+
node: child,
|
|
51
|
+
message: "Expected a space after comma.",
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
} else if (isWhitespace(next)) {
|
|
56
|
+
context.report({
|
|
57
|
+
node: next,
|
|
58
|
+
message: "Unexpected space after comma.",
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/rules/spacing-operators.js
|
|
2
|
+
|
|
3
|
+
import { wantsSpacesAroundOperator } from "../utils/operatorSpacing.js";
|
|
4
|
+
|
|
5
|
+
function isDirectComment(child) {
|
|
6
|
+
return child?.type === "LeafNode" && child.kind === "Token`Comment";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
name: "spacing-operators",
|
|
11
|
+
description:
|
|
12
|
+
"Operator spacing inconsistent with wolfram.spaceAroundOperators option",
|
|
13
|
+
defaultLevel: "warn",
|
|
14
|
+
fixableByFormatter: true,
|
|
15
|
+
|
|
16
|
+
visit(node, context) {
|
|
17
|
+
if (
|
|
18
|
+
node.type !== "InfixNode" &&
|
|
19
|
+
node.type !== "BinaryNode" &&
|
|
20
|
+
node.type !== "TernaryNode"
|
|
21
|
+
)
|
|
22
|
+
return;
|
|
23
|
+
const children = node.children ?? [];
|
|
24
|
+
if (children.some(isDirectComment)) return;
|
|
25
|
+
|
|
26
|
+
const isSpaceTrivia = (child) =>
|
|
27
|
+
child?.type === "LeafNode" &&
|
|
28
|
+
[
|
|
29
|
+
"Token`Whitespace",
|
|
30
|
+
"Whitespace",
|
|
31
|
+
"Token`Newline",
|
|
32
|
+
"Newline",
|
|
33
|
+
].includes(child.kind);
|
|
34
|
+
const ignoredOperatorKinds = new Set([
|
|
35
|
+
"Token`Comma",
|
|
36
|
+
"Token`Semi",
|
|
37
|
+
"Token`OpenSquare",
|
|
38
|
+
"Token`CloseSquare",
|
|
39
|
+
"Token`OpenParen",
|
|
40
|
+
"Token`CloseParen",
|
|
41
|
+
"Token`OpenCurly",
|
|
42
|
+
"Token`CloseCurly",
|
|
43
|
+
"Token`Amp",
|
|
44
|
+
"Token`Under",
|
|
45
|
+
"Token`UnderUnder",
|
|
46
|
+
"Token`UnderUnderUnder",
|
|
47
|
+
"Token`Blank",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < children.length; i++) {
|
|
51
|
+
const c = children[i];
|
|
52
|
+
if (c.type !== "LeafNode" || !c.kind.startsWith("Token`")) continue;
|
|
53
|
+
const isOp =
|
|
54
|
+
!ignoredOperatorKinds.has(c.kind) &&
|
|
55
|
+
![
|
|
56
|
+
"Token`Whitespace",
|
|
57
|
+
"Whitespace",
|
|
58
|
+
"Token`Newline",
|
|
59
|
+
"Newline",
|
|
60
|
+
].includes(c.kind) &&
|
|
61
|
+
!c.kind.startsWith("Token`Comment") &&
|
|
62
|
+
!c.kind.startsWith("Token`Fake`") &&
|
|
63
|
+
!c.kind.startsWith("Token`Hash");
|
|
64
|
+
if (!isOp) continue;
|
|
65
|
+
const wantSpaces = wantsSpacesAroundOperator(
|
|
66
|
+
node,
|
|
67
|
+
context.options,
|
|
68
|
+
c,
|
|
69
|
+
);
|
|
70
|
+
const prevIsWhitespace = i > 0 && isSpaceTrivia(children[i - 1]);
|
|
71
|
+
const nextIsWhitespace =
|
|
72
|
+
i < children.length - 1 && isSpaceTrivia(children[i + 1]);
|
|
73
|
+
const ok = wantSpaces
|
|
74
|
+
? prevIsWhitespace && nextIsWhitespace
|
|
75
|
+
: !prevIsWhitespace && !nextIsWhitespace;
|
|
76
|
+
if (!ok) {
|
|
77
|
+
context.report({
|
|
78
|
+
node: c,
|
|
79
|
+
message: wantSpaces
|
|
80
|
+
? `Expected spaces around operator "${c.value}".`
|
|
81
|
+
: `Expected no spaces around operator "${c.value}".`,
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { doc } from "prettier";
|
|
2
|
+
import { isComment } from "./nodes/leaf.js";
|
|
3
|
+
import { sourceLineGap } from "./sourceLines.js";
|
|
4
|
+
|
|
5
|
+
const { hardline } = doc.builders;
|
|
6
|
+
|
|
7
|
+
function sourceTextBetween(leftNode, rightNode, options) {
|
|
8
|
+
if (
|
|
9
|
+
typeof options?.originalText !== "string" ||
|
|
10
|
+
typeof leftNode?.locEnd !== "number" ||
|
|
11
|
+
typeof rightNode?.locStart !== "number" ||
|
|
12
|
+
leftNode.locEnd > rightNode.locStart
|
|
13
|
+
) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return options.originalText.slice(leftNode.locEnd, rightNode.locStart);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sourceColumnsTouch(leftNode, rightNode) {
|
|
21
|
+
const leftEnd = leftNode?.source?.[1];
|
|
22
|
+
const rightStart = rightNode?.source?.[0];
|
|
23
|
+
if (!Array.isArray(leftEnd) || !Array.isArray(rightStart)) return false;
|
|
24
|
+
return leftEnd[0] === rightStart[0] && leftEnd[1] === rightStart[1];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function areTightAdjacentComments(leftNode, rightNode, options) {
|
|
28
|
+
if (!isComment(leftNode) || !isComment(rightNode)) return false;
|
|
29
|
+
|
|
30
|
+
const between = sourceTextBetween(leftNode, rightNode, options);
|
|
31
|
+
if (between != null) return between === "";
|
|
32
|
+
|
|
33
|
+
return sourceColumnsTouch(leftNode, rightNode);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function sameLineCommentSeparator(leftNode, rightNode, options) {
|
|
37
|
+
return areTightAdjacentComments(leftNode, rightNode, options) ? "" : " ";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function commentBoundarySeparator(
|
|
41
|
+
leftNode,
|
|
42
|
+
rightNode,
|
|
43
|
+
options,
|
|
44
|
+
fallback,
|
|
45
|
+
) {
|
|
46
|
+
if (!rightNode) return "";
|
|
47
|
+
const gap = sourceLineGap(leftNode, rightNode, options);
|
|
48
|
+
if (gap === 0) return sameLineCommentSeparator(leftNode, rightNode, options);
|
|
49
|
+
if (gap > 0) return hardline;
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { doc } from "prettier";
|
|
2
|
+
import { normalizeWolframOptions } from "../options.js";
|
|
3
|
+
import { sameLineCommentSeparator } from "./commentSpacing.js";
|
|
4
|
+
|
|
5
|
+
export function joinDocsWithSpace(docs) {
|
|
6
|
+
const nonEmptyDocs = docs.filter(
|
|
7
|
+
(docNode) => docNode !== "" && docNode != null,
|
|
8
|
+
);
|
|
9
|
+
if (nonEmptyDocs.length === 0) return "";
|
|
10
|
+
|
|
11
|
+
const joined = [nonEmptyDocs[0]];
|
|
12
|
+
for (let i = 1; i < nonEmptyDocs.length; i++) {
|
|
13
|
+
joined.push(" ", nonEmptyDocs[i]);
|
|
14
|
+
}
|
|
15
|
+
return joined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function joinCommentDocs(comments, options) {
|
|
19
|
+
const nonEmptyComments = comments.filter(
|
|
20
|
+
(comment) => comment?.doc !== "" && comment?.doc != null,
|
|
21
|
+
);
|
|
22
|
+
if (nonEmptyComments.length === 0) return "";
|
|
23
|
+
|
|
24
|
+
const joined = [nonEmptyComments[0].doc];
|
|
25
|
+
for (let i = 1; i < nonEmptyComments.length; i++) {
|
|
26
|
+
joined.push(
|
|
27
|
+
sameLineCommentSeparator(
|
|
28
|
+
nonEmptyComments[i - 1].node,
|
|
29
|
+
nonEmptyComments[i].node,
|
|
30
|
+
options,
|
|
31
|
+
),
|
|
32
|
+
nonEmptyComments[i].doc,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return joined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderFlatDoc(docNode, options) {
|
|
39
|
+
const rendered = doc.printer.printDocToString(docNode, {
|
|
40
|
+
printWidth: 100000,
|
|
41
|
+
tabWidth: options.tabWidth ?? 2,
|
|
42
|
+
useTabs: false,
|
|
43
|
+
endOfLine: "lf",
|
|
44
|
+
}).formatted;
|
|
45
|
+
return rendered.endsWith("\n") ? rendered.slice(0, -1) : rendered;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function documentationCommentColumn(
|
|
49
|
+
entries,
|
|
50
|
+
options,
|
|
51
|
+
suffixForEntry = () => "",
|
|
52
|
+
) {
|
|
53
|
+
options = normalizeWolframOptions(options);
|
|
54
|
+
const manual = options.wolframDocumentationCommentColumn ?? 0;
|
|
55
|
+
if (manual > 0) return manual;
|
|
56
|
+
const padding = Math.max(
|
|
57
|
+
1,
|
|
58
|
+
options.wolframDocumentationCommentPadding ?? 2,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
let maxCodeWidth = 0;
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (!entry.trailingCommentDoc) continue;
|
|
64
|
+
const rendered = renderFlatDoc(
|
|
65
|
+
[entry.doc, suffixForEntry(entry)],
|
|
66
|
+
options,
|
|
67
|
+
);
|
|
68
|
+
if (rendered.includes("\n")) continue;
|
|
69
|
+
maxCodeWidth = Math.max(maxCodeWidth, rendered.length);
|
|
70
|
+
}
|
|
71
|
+
return maxCodeWidth + padding;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function withAlignedTrailingComment(
|
|
75
|
+
entry,
|
|
76
|
+
options,
|
|
77
|
+
column,
|
|
78
|
+
suffix = "",
|
|
79
|
+
) {
|
|
80
|
+
if (!entry.trailingCommentDoc) return [entry.doc, suffix];
|
|
81
|
+
|
|
82
|
+
const rendered = renderFlatDoc([entry.doc, suffix], options);
|
|
83
|
+
if (rendered.includes("\n")) {
|
|
84
|
+
return [entry.doc, suffix, " ", entry.trailingCommentDoc];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const gap = Math.max(1, column - rendered.length);
|
|
88
|
+
return [rendered, " ".repeat(gap), entry.trailingCommentDoc];
|
|
89
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/translator/index.js
|
|
2
|
+
import { doc } from "prettier";
|
|
3
|
+
import { printLeaf } from "./nodes/leaf.js";
|
|
4
|
+
import { printContainer } from "./nodes/container.js";
|
|
5
|
+
import { printCall } from "./nodes/call.js";
|
|
6
|
+
import { printInfix } from "./nodes/infix.js";
|
|
7
|
+
import { printBinary } from "./nodes/binary.js";
|
|
8
|
+
import { printPrefix } from "./nodes/prefix.js";
|
|
9
|
+
import { printPostfix } from "./nodes/postfix.js";
|
|
10
|
+
import { printCompound } from "./nodes/compound.js";
|
|
11
|
+
import { printGroup } from "./nodes/group.js";
|
|
12
|
+
import { printTernary } from "./nodes/ternary.js";
|
|
13
|
+
import { getSpecialPrinter } from "./specialForms.js";
|
|
14
|
+
import { printOriginalSource } from "./sourcePreservation.js";
|
|
15
|
+
|
|
16
|
+
const { hardline } = doc.builders;
|
|
17
|
+
|
|
18
|
+
function withTrailingNewline(docNode, options) {
|
|
19
|
+
if (!options?.wolframTrailingNewline || docNode === "") return docNode;
|
|
20
|
+
return [docNode, hardline];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* prettier's print function — called as path.call(print, ...) by prettier core.
|
|
25
|
+
* `path` is a FastPath; `path.getValue()` returns the current node.
|
|
26
|
+
*/
|
|
27
|
+
export function printNode(path, options, print) {
|
|
28
|
+
const node = path.getValue();
|
|
29
|
+
if (!node) return "";
|
|
30
|
+
|
|
31
|
+
switch (node.type) {
|
|
32
|
+
case "ContainerNode": {
|
|
33
|
+
const body = printContainer(node, options, (child) => {
|
|
34
|
+
return path.call(
|
|
35
|
+
print,
|
|
36
|
+
"children",
|
|
37
|
+
node.children.indexOf(child),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
const fileDoc = node.wlsShebang
|
|
41
|
+
? body === ""
|
|
42
|
+
? node.wlsShebang
|
|
43
|
+
: [node.wlsShebang, hardline, body]
|
|
44
|
+
: body;
|
|
45
|
+
return withTrailingNewline(fileDoc, options);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
case "LeafNode":
|
|
49
|
+
return printLeaf(node, options, { path });
|
|
50
|
+
|
|
51
|
+
case "CallNode": {
|
|
52
|
+
const special = getSpecialPrinter(node, options);
|
|
53
|
+
if (special) return special(path, options, print, node);
|
|
54
|
+
return printCall(path, options, print, node);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case "InfixNode": {
|
|
58
|
+
const special = getSpecialPrinter(node, options);
|
|
59
|
+
if (special) return special(path, options, print, node);
|
|
60
|
+
return printInfix(node, options, (child) =>
|
|
61
|
+
path.call(print, "children", node.children.indexOf(child)),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case "BinaryNode":
|
|
66
|
+
return printBinary(node, options, (child) =>
|
|
67
|
+
path.call(print, "children", node.children.indexOf(child)),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
case "PrefixNode":
|
|
71
|
+
return printPrefix(node, options, (child) =>
|
|
72
|
+
path.call(print, "children", node.children.indexOf(child)),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
case "PostfixNode":
|
|
76
|
+
return printPostfix(node, options, (child) =>
|
|
77
|
+
path.call(print, "children", node.children.indexOf(child)),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
case "CompoundNode":
|
|
81
|
+
return printCompound(node, options, (child) =>
|
|
82
|
+
path.call(print, "children", node.children.indexOf(child)),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
case "GroupNode":
|
|
86
|
+
return printGroup(path, options, print, node);
|
|
87
|
+
|
|
88
|
+
case "TernaryNode":
|
|
89
|
+
return printTernary(node, options, (child) =>
|
|
90
|
+
path.call(print, "children", node.children.indexOf(child)),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
default:
|
|
94
|
+
// Unsupported CST nodes should round-trip the original source instead
|
|
95
|
+
// of exposing raw CodeParser internals in formatter output.
|
|
96
|
+
return printOriginalSource(node, options);
|
|
97
|
+
}
|
|
98
|
+
}
|