@prettier/plugin-ruby
Version:
prettier plugin for the Ruby programming language
96 lines (86 loc) • 3.18 kB
JavaScript
const {
align,
concat,
group,
hardline,
ifBreak,
indent,
softline
} = require("../prettier");
const { containsAssignment } = require("../utils");
const printLoop = (keyword, modifier) => (path, { inlineLoops }, print) => {
let inlineParts = [
path.call(print, "body", 1),
` ${keyword} `,
path.call(print, "body", 0)
];
// If the return value of this loop expression is being assigned to anything
// besides a local variable then we can't inline the entire expression
// without wrapping it in parentheses. This is because the following
// expressions have different semantic meaning:
//
// hash[:key] = break :value while false
// hash[:key] = while false do break :value end
//
// The first one will not result in an empty hash, whereas the second one
// will result in `{ key: nil }`. In this case what we need to do for the
// first expression to align is wrap it in parens, as in:
//
// hash[:key] = (break :value while false)
if (["assign", "massign"].includes(path.getParentNode().type)) {
inlineParts = ["("].concat(inlineParts).concat(")");
}
const inlineLoop = concat(inlineParts);
// If we're in the modifier form and we're modifying a `begin`, then this is a
// special case where we need to explicitly use the modifier form because
// otherwise the semantic meaning changes. This looks like:
//
// begin
// foo
// end while bar
//
// The above is effectively a `do...while` loop (which we don't have in ruby).
if (modifier && path.getValue().body[1].type === "begin") {
return inlineLoop;
}
const blockLoop = concat([
concat([
`${keyword} `,
align(keyword.length + 1, path.call(print, "body", 0))
]),
indent(concat([softline, path.call(print, "body", 1)])),
concat([softline, "end"])
]);
// If we're disallowing inline loops or if the predicate of the loop contains
// an assignment (in which case we can't know for certain that that
// assignment doesn't impact the statements inside the loop) then we can't
// use the modifier form and we must use the block form.
if (!inlineLoops || containsAssignment(path.getValue().body[0])) {
return blockLoop;
}
return group(ifBreak(blockLoop, inlineLoop));
};
// Technically this is incorrect. A `for` loop actually introduces and modifies
// a local variable that then remains in the outer scope. Additionally, if the
// `each` method was somehow missing from the enumerable (it's possible...),
// then this transformation would fail. However - I've never actually seen a
// `for` loop used in production. If someone actually calls me on it, I'll fix
// this, but for now I'm leaving it.
const printFor = (path, opts, print) =>
group(
concat([
path.call(print, "body", 1),
".each do |",
path.call(print, "body", 0),
"|",
indent(concat([hardline, path.call(print, "body", 2)])),
concat([hardline, "end"])
])
);
module.exports = {
while: printLoop("while", false),
while_mod: printLoop("while", true),
until: printLoop("until", false),
until_mod: printLoop("until", true),
for: printFor
};