@prettier/plugin-ruby
Advanced tools
Comparing version 1.5.5 to 1.6.0
@@ -9,2 +9,17 @@ # Changelog | ||
## [1.6.0] - 2021-06-23 | ||
### Added | ||
- [#859](https://github.com/prettier/plugin-ruby/issues/859) - azz, kddeisz - Support the `--insert-pragma` option for the incremental adoption workflow. | ||
- [#904](https://github.com/prettier/plugin-ruby/issues/904) - Hansenq, kddeisz - Support the `%s` symbol literal syntax. | ||
- [#833](https://github.com/prettier/plugin-ruby/issues/883) - kddeisz - Support for prettier >= v2.3.0. | ||
### Changed | ||
- [#854](https://github.com/prettier/plugin-ruby/issues/854) - jflinter, kddeisz - Parentheses should not be stripped from optional RBS union types. | ||
- [#888](https://github.com/prettier/plugin-ruby/issues/888) - MaxNotarangelo, kddeisz - Ensure parentheses wrap conditionals that get transformed into the modifier form when they're used within a binary node. | ||
- [#874](https://github.com/prettier/plugin-ruby/issues/874) - yratanov, kddeisz - Ensure that method calls chained onto the ends of blocks still print their arguments. | ||
- [#897](https://github.com/prettier/plugin-ruby/pull/897) - Hansenq - Reenable the `Layout/LineLength` rubocop rule in our shipped config so that line lengths for other cops are calculated correctly. | ||
## [1.5.5] - 2021-03-25 | ||
@@ -1108,3 +1123,4 @@ | ||
[unreleased]: https://github.com/prettier/plugin-ruby/compare/v1.5.5...HEAD | ||
[unreleased]: https://github.com/prettier/plugin-ruby/compare/v1.6.0...HEAD | ||
[1.6.0]: https://github.com/prettier/plugin-ruby/compare/v1.5.5...v1.6.0 | ||
[1.5.5]: https://github.com/prettier/plugin-ruby/compare/v1.5.4...v1.5.5 | ||
@@ -1111,0 +1127,0 @@ [1.5.4]: https://github.com/prettier/plugin-ruby/compare/v1.5.3...v1.5.4 |
@@ -78,3 +78,3 @@ # Contributing | ||
As the nodes are printing themselves and their children, they're additionally building up a second AST. That AST is built using the `builder` commands from prettier core, described [here](https://github.com/prettier/prettier/blob/master/commands.md). As an example, below is how a `binary` node (like the one representing the `1 + 1` above) would handle printing itself: | ||
As the nodes are printing themselves and their children, they're additionally building up a second AST. That AST is built using the `builder` commands from prettier core, described [here](https://github.com/prettier/prettier/blob/main/commands.md). As an example, below is how a `binary` node (like the one representing the `1 + 1` above) would handle printing itself: | ||
@@ -163,3 +163,3 @@ ```javascript | ||
- [Prettier plugin documentation](https://prettier.io/docs/en/plugins.html) - documentation around `prettier`'s plugin system | ||
- [Builder commands](https://github.com/prettier/prettier/blob/master/commands.md) - the functions used to build the `prettier` IR | ||
- [Builder commands](https://github.com/prettier/prettier/blob/main/commands.md) - the functions used to build the `prettier` IR | ||
- [Writing a Prettier plugin](https://medium.com/@fvictorio/how-to-write-a-plugin-for-prettier-a0d98c845e70) - a nice tutorial on how to build a `prettier` plugin | ||
@@ -166,0 +166,0 @@ |
{ | ||
"name": "@prettier/plugin-ruby", | ||
"version": "1.5.5", | ||
"version": "1.6.0", | ||
"description": "prettier plugin for the Ruby programming language", | ||
@@ -27,4 +27,4 @@ "main": "src/plugin.js", | ||
"eslint-config-prettier": "^8.0.0", | ||
"husky": "^5.0.9", | ||
"jest": "^26.0.0", | ||
"husky": "^6.0.0", | ||
"jest": "^27.0.1", | ||
"pretty-quick": "^3.1.0" | ||
@@ -31,0 +31,0 @@ }, |
@@ -131,14 +131,14 @@ <div align="center"> | ||
| API Option | CLI Option | Default | Description | | ||
| ------------------- | ----------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `printWidth` | `--print-width` | `80` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#print-width)). | | ||
| `requirePragma` | `--require-pragma` | `false` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#require-pragma)). | | ||
| `rubyArrayLiteral` | `--ruby-array-literal` | `true` | When possible, favor the use of string and symbol array literals. | | ||
| `rubyHashLabel` | `--ruby-hash-label` | `true` | When possible, uses the shortened hash key syntax, as opposed to hash rockets. | | ||
| `rubyModifier` | `--ruby-modifier` | `true` | When it fits on one line, allows while and until statements to use the modifier form. | | ||
| `rubyNetcatCommand` | `--ruby-netcat-command` | | The prefix of the command to execute to communicate between the node.js process and the Ruby process. (For example, `"nc -U"` or `"telnet -u"`) Normally you should not set this option. | | ||
| `rubySingleQuote` | `--ruby-single-quote` | `true` | When double quotes are not necessary for interpolation, prefers the use of single quotes for string literals. | | ||
| `rubyToProc` | `--ruby-to-proc` | `false` | When possible, convert blocks to the more concise `Symbol#to_proc` syntax. | | ||
| `tabWidth` | `--tab-width` | `2` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#tab-width)). | | ||
| `trailingComma` | `--trailing-comma` | `false` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#trailing-comma)). `"es5"` is equivalent to `true`. | | ||
| API Option | CLI Option | Default | Description | | ||
| ------------------- | ----------------------- | :------: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `printWidth` | `--print-width` | `80` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#print-width)). | | ||
| `requirePragma` | `--require-pragma` | `false` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#require-pragma)). | | ||
| `rubyArrayLiteral` | `--ruby-array-literal` | `true` | When possible, favor the use of string and symbol array literals. | | ||
| `rubyHashLabel` | `--ruby-hash-label` | `true` | When possible, uses the shortened hash key syntax, as opposed to hash rockets. | | ||
| `rubyModifier` | `--ruby-modifier` | `true` | When it fits on one line, allows while and until statements to use the modifier form. | | ||
| `rubyNetcatCommand` | `--ruby-netcat-command` | | The prefix of the command to execute to communicate between the node.js process and the Ruby process. (For example, `"nc -U"` or `"telnet -u"`) Normally you should not set this option. | | ||
| `rubySingleQuote` | `--ruby-single-quote` | `true` | When double quotes are not necessary for interpolation, prefers the use of single quotes for string literals. | | ||
| `rubyToProc` | `--ruby-to-proc` | `false` | When possible, convert blocks to the more concise `Symbol#to_proc` syntax. | | ||
| `tabWidth` | `--tab-width` | `2` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#tab-width)). | | ||
| `trailingComma` | `--trailing-comma` | `"none"` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#trailing-comma)). `"es5"` is equivalent to `true`. | | ||
@@ -182,2 +182,21 @@ Any of these can be added to your existing [prettier configuration | ||
### Usage with an editor | ||
For [supported editor integrations](https://github.com/prettier/prettier/blob/main/website/data/editors.yml), you should follow the instructions for installing the integration, then install the npm version of this plugin as a development dependency of your project. For most integrations, that should be sufficient. For convenience, the instructions for integrating with VSCode are used as an example below: | ||
- Install the [Prettier - Code Formatter](https://github.com/prettier/prettier-vscode) extension. | ||
- Add the npm `@prettier/plugin-ruby` package to your project as described above. | ||
- Configure in your `settings.json` (`formatOnSave` is optional): | ||
```json | ||
{ | ||
"[ruby]": { | ||
"editor.defaultFormatter": "esbenp.prettier-vscode", | ||
"editor.formatOnSave": true | ||
} | ||
} | ||
``` | ||
Refer to [this issue](https://github.com/prettier/plugin-ruby/issues/113#issuecomment-783426539) if you're having difficulties. | ||
## Contributing | ||
@@ -184,0 +203,0 @@ |
const parseSync = require("../parser/parseSync"); | ||
const parse = (text, _parsers, opts) => { | ||
function parse(text, _parsers, opts) { | ||
return parseSync("haml", text, opts); | ||
}; | ||
} | ||
const pragmaPattern = /^\s*-#\s*@(prettier|format)/; | ||
const hasPragma = (text) => pragmaPattern.test(text); | ||
function hasPragma(text) { | ||
return /^\s*-#\s*@(prettier|format)/.test(text); | ||
} | ||
@@ -10,0 +11,0 @@ // These functions are just placeholders until we can actually perform this |
@@ -0,28 +1,438 @@ | ||
const { | ||
align, | ||
concat, | ||
fill, | ||
group, | ||
hardline, | ||
ifBreak, | ||
indent, | ||
join, | ||
line, | ||
softline | ||
} = require("../prettier"); | ||
const embed = require("./embed"); | ||
const nodes = { | ||
comment: require("./nodes/comment"), | ||
doctype: require("./nodes/doctype"), | ||
filter: require("./nodes/filter"), | ||
haml_comment: require("./nodes/hamlComment"), | ||
plain: require("./nodes/plain"), | ||
root: require("./nodes/root"), | ||
script: require("./nodes/script"), | ||
silent_script: require("./nodes/silentScript"), | ||
tag: require("./nodes/tag") | ||
const docTypes = { | ||
basic: "Basic", | ||
frameset: "Frameset", | ||
mobile: "Mobile", | ||
rdfa: "RDFa", | ||
strict: "Strict", | ||
xml: "XML" | ||
}; | ||
const genericPrint = (path, opts, print) => { | ||
const { type } = path.getValue(); | ||
const docVersions = ["1.1", "5"]; | ||
/* istanbul ignore next */ | ||
if (!(type in nodes)) { | ||
throw new Error(`Unsupported node encountered: ${type}`); | ||
// Prints out a hash key according to the configured prettier options. | ||
function printHashKey(key, opts) { | ||
let quoted = key; | ||
const joiner = opts.rubyHashLabel ? ":" : " =>"; | ||
if (key.includes(":") || key.includes("-")) { | ||
const quote = opts.rubySingleQuote ? "'" : '"'; | ||
quoted = `${quote}${key}${quote}`; | ||
} | ||
return nodes[type](path, opts, print); | ||
}; | ||
return `${opts.rubyHashLabel ? "" : ":"}${quoted}${joiner}`; | ||
} | ||
// Prints out the value inside of a hash key-value pair according to the | ||
// configured prettier options. | ||
function printHashValue(value, opts) { | ||
if (typeof value !== "string") { | ||
return value.toString(); | ||
} | ||
// This is a very special syntax created by the parser to let us know that | ||
// this should be printed literally instead of as a string. | ||
if (value.startsWith("&")) { | ||
return value.slice(1); | ||
} | ||
const quote = opts.rubySingleQuote && !value.includes("#{") ? "'" : '"'; | ||
return `${quote}${value}${quote}`; | ||
} | ||
// This will print an attributes object to a Doc node. It handles nesting on | ||
// multiple levels and will print out according to whether or not the version of | ||
// HAML being used supports multi-line attributes. | ||
function printAttributes(object, opts, level = 0) { | ||
if (typeof object !== "object") { | ||
return printHashValue(object, opts); | ||
} | ||
const boundary = level === 0 ? softline : line; | ||
const parts = Object.keys(object).map((key) => | ||
concat([ | ||
printHashKey(key, opts), | ||
" ", | ||
printAttributes(object[key], opts, level + 1) | ||
]) | ||
); | ||
// If we have support for multi-line attributes laid out like a regular hash, | ||
// then we print them that way here. | ||
if (opts.supportsMultiline) { | ||
return group( | ||
concat([ | ||
"{", | ||
indent(group(concat([boundary, join(concat([",", line]), parts)]))), | ||
boundary, | ||
"}" | ||
]) | ||
); | ||
} | ||
// Otherwise, if we only have one attribute, then just print it inline | ||
// regardless of how long it is. | ||
if (parts.length === 0) { | ||
return group(concat(["{", parts[0], "}"])); | ||
} | ||
// Otherwise, depending on how long the line is it will split the content into | ||
// multi-line attributes that old Haml understands. | ||
return group( | ||
concat([ | ||
"{", | ||
parts[0], | ||
",", | ||
align( | ||
opts.headerLength + 1, | ||
concat([line, join(concat([",", line]), parts.slice(1))]) | ||
), | ||
"}" | ||
]) | ||
); | ||
} | ||
// A utility function used in a silent script that is meant to determine if a | ||
// child node is a continuation of a parent node (as in a when clause within a | ||
// case statement or an else clause within an if). | ||
function isContinuation(parentNode, childNode) { | ||
if (childNode.type !== "silent_script") { | ||
return false; | ||
} | ||
const parent = parentNode.value.keyword; | ||
const child = childNode.value.keyword; | ||
return ( | ||
(parent === "case" && ["when", "else"].includes(child)) || | ||
(["if", "unless"].includes(parent) && ["elsif", "else"].includes(child)) | ||
); | ||
} | ||
// This is our printer's main print function that will switch on the type of | ||
// node and print it out by returning a Doc tree. | ||
function printNode(path, opts, print) { | ||
const node = path.getValue(); | ||
const { value } = node; | ||
switch (node.type) { | ||
case "comment": | ||
return printComment(); | ||
case "doctype": | ||
return printDoctype(); | ||
case "filter": | ||
return printFilter(); | ||
case "haml_comment": | ||
return printHamlComment(); | ||
case "plain": | ||
return printPlain(); | ||
case "root": | ||
return printRoot(); | ||
case "script": | ||
return printScript(); | ||
case "silent_script": | ||
return printSilentScript(); | ||
case "tag": | ||
return printTag(); | ||
default: | ||
throw new Error(`Unsupported node encountered: ${node.type}`); | ||
} | ||
// It's common to a couple of nodes to attach nested child nodes on the | ||
// children property. This utility prints them out grouped together with their | ||
// parent node docs. | ||
function printWithChildren(docs) { | ||
if (node.children.length === 0) { | ||
return docs; | ||
} | ||
return group( | ||
concat([ | ||
docs, | ||
indent(concat([hardline, join(hardline, path.map(print, "children"))])) | ||
]) | ||
); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#html-comments- | ||
function printComment() { | ||
const parts = ["/"]; | ||
if (value.revealed) { | ||
parts.push("!"); | ||
} | ||
if (value.conditional) { | ||
parts.push(value.conditional); | ||
} else if (value.text) { | ||
parts.push(" ", value.text); | ||
} | ||
return printWithChildren(group(concat(parts))); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#doctype- | ||
function printDoctype() { | ||
const parts = ["!!!"]; | ||
if (value.type in docTypes) { | ||
parts.push(docTypes[value.type]); | ||
} else if (docVersions.includes(value.version)) { | ||
parts.push(value.version); | ||
} else { | ||
parts.push(value.type); | ||
} | ||
if (value.encoding) { | ||
parts.push(value.encoding); | ||
} | ||
return group(join(" ", parts)); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#filters | ||
function printFilter() { | ||
return group( | ||
concat([ | ||
":", | ||
value.name, | ||
indent( | ||
concat([hardline, join(hardline, value.text.trim().split("\n"))]) | ||
) | ||
]) | ||
); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#haml-comments-- | ||
function printHamlComment() { | ||
const parts = ["-#"]; | ||
if (value.text) { | ||
if (opts.originalText.split("\n")[node.line - 1].trim() === "-#") { | ||
const lines = value.text.trim().split("\n"); | ||
parts.push(indent(concat([hardline, join(hardline, lines)]))); | ||
} else { | ||
parts.push(" ", value.text.trim()); | ||
} | ||
} | ||
return concat(parts); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#plain-text | ||
function printPlain() { | ||
return value.text; | ||
} | ||
// The root node in the AST that we build in the parser. | ||
function printRoot() { | ||
return concat([join(hardline, path.map(print, "children")), hardline]); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#inserting_ruby | ||
function printScript() { | ||
const parts = []; | ||
if (value.escape_html) { | ||
parts.unshift("&"); | ||
} | ||
if (value.preserve) { | ||
parts.push("~"); | ||
} else if (!value.interpolate) { | ||
parts.push("="); | ||
} | ||
if (value.escape_html && !value.preserve && value.interpolate) { | ||
parts.push(" ", value.text.trim().slice(1, -1)); | ||
} else { | ||
parts.push(" ", value.text.trim()); | ||
} | ||
return printWithChildren(group(concat(parts))); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#running-ruby-- | ||
function printSilentScript() { | ||
const parts = [`- ${value.text.trim()}`]; | ||
if (node.children.length > 0) { | ||
parts.push( | ||
concat( | ||
path.map((childPath) => { | ||
const child = childPath.getValue(); | ||
const concated = concat([hardline, print(childPath)]); | ||
return isContinuation(node, child) ? concated : indent(concated); | ||
}, "children") | ||
) | ||
); | ||
} | ||
return group(concat(parts)); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#element-name- | ||
function printTag() { | ||
const { attributes, dynamic_attributes } = value; | ||
const parts = []; | ||
// If we have a tag that isn't a div, then we need to print out that name of | ||
// that tag first. If it is a div, first we'll check if there are any other | ||
// things that would force us to print out the div explicitly, and otherwise | ||
// we'll leave it off. | ||
if (value.name !== "div") { | ||
parts.push(`%${value.name}`); | ||
} | ||
// If we have a class attribute, then we're going to print that here using | ||
// the special class syntax. | ||
if (attributes.class) { | ||
parts.push(`.${attributes.class.replace(/ /g, ".")}`); | ||
} | ||
// If we have an id attribute, then we're going to print that here using the | ||
// special id syntax. | ||
if (attributes.id) { | ||
parts.push(`#${attributes.id}`); | ||
} | ||
// If we're using dynamic attributes on this tag, then they come in as a | ||
// string that looks like the output of Hash#inspect from Ruby. So here | ||
// we're going to split it all up and print it out nicely. | ||
if (dynamic_attributes.new) { | ||
const pairs = dynamic_attributes.new | ||
.slice(1, -2) | ||
.split(",") | ||
.map((pair) => join("=", pair.slice(1).split('" => '))); | ||
parts.push( | ||
group( | ||
concat([ | ||
"(", | ||
align(parts.join("").length + 1, fill(join(line, pairs).parts)), | ||
")" | ||
]) | ||
) | ||
); | ||
} | ||
// If there are any static attributes that are not class or id (because we | ||
// already took care of those), then we're going to print them out here. | ||
const staticAttributes = Object.keys(attributes).filter( | ||
(name) => !["class", "id"].includes(name) | ||
); | ||
if (staticAttributes.length > 0) { | ||
const docs = staticAttributes.reduce((accum, key) => { | ||
const doc = `${printHashKey(key, opts)} ${printHashValue( | ||
attributes[key], | ||
opts | ||
)}`; | ||
return accum.length === 0 ? [doc] : accum.concat(",", line, doc); | ||
}, []); | ||
parts.push( | ||
group(concat(["{", align(parts.join("").length + 1, fill(docs)), "}"])) | ||
); | ||
} | ||
// If there are dynamic attributes that don't use the newer syntax, then | ||
// we're going to print them out here. | ||
if (dynamic_attributes.old) { | ||
if (parts.length === 0) { | ||
parts.push("%div"); | ||
} | ||
if (typeof dynamic_attributes.old === "string") { | ||
parts.push(dynamic_attributes.old); | ||
} else { | ||
const attrOptions = { | ||
// This is kind of a total hack in that I don't think you're really | ||
// supposed to directly use `path.stack`, but it's the easiest way to | ||
// get the root node without having to know how many levels deep we | ||
// are. | ||
supportsMultiline: path.stack[0].supports_multiline, | ||
headerLength: parts.join("").length | ||
}; | ||
parts.push( | ||
printAttributes( | ||
dynamic_attributes.old, | ||
Object.assign({}, opts, attrOptions) | ||
) | ||
); | ||
} | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#object-reference- | ||
if (value.object_ref) { | ||
if (parts.length === 0) { | ||
parts.push("%div"); | ||
} | ||
parts.push(value.object_ref); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#whitespace-removal--and- | ||
if (value.nuke_outer_whitespace) { | ||
parts.push(">"); | ||
} | ||
if (value.nuke_inner_whitespace) { | ||
parts.push("<"); | ||
} | ||
// https://haml.info/docs/yardoc/file.REFERENCE.html#empty-void-tags- | ||
if (value.self_closing) { | ||
parts.push("/"); | ||
} | ||
if (value.value) { | ||
const prefix = value.parse ? "= " : ifBreak("", " "); | ||
return printWithChildren( | ||
group( | ||
concat([ | ||
group(concat(parts)), | ||
indent(concat([softline, prefix, value.value])) | ||
]) | ||
) | ||
); | ||
} | ||
// In case none of the other if statements have matched and we're printing | ||
// a div, we need to explicitly add it back into the array. | ||
if (parts.length === 0 && value.name === "div") { | ||
parts.push("%div"); | ||
} | ||
return printWithChildren(group(concat(parts))); | ||
} | ||
} | ||
// This function handles adding the format pragma to a source string. This is an | ||
// optional workflow for incremental adoption. | ||
function insertPragma(text) { | ||
return `-# @format${text.startsWith("-#") ? "\n" : "\n\n"}${text}`; | ||
} | ||
module.exports = { | ||
embed, | ||
print: genericPrint | ||
print: printNode, | ||
insertPragma | ||
}; |
@@ -138,11 +138,13 @@ const { spawn, spawnSync, execSync } = require("child_process"); | ||
stderr.includes("invalid option -- U") || | ||
stderr.includes("invalid option -- 'u'") || | ||
stderr.includes("Protocol not supported") | ||
) { | ||
throw new Error(` | ||
@prettier/plugin-ruby uses netcat to communicate over unix sockets between | ||
the node.js process running prettier and an underlying Ruby process used | ||
for parsing. Unfortunately the version of netcat that you have installed | ||
does not support unix sockets. To solve this either uninstall the version | ||
of netcat that you're using and use a different implementation, or change | ||
the value of the rubyNetcatCommand option in your prettier configuration. | ||
@prettier/plugin-ruby uses unix sockets to communicate between the node.js | ||
process running prettier and an underlying Ruby process used for parsing. | ||
Unfortunately the command that it tried to use to do that | ||
(${netcat.command}) does not support unix sockets. To solve this either | ||
uninstall the version of ${netcat.command} that you're using and use a | ||
different implementation, or change the value of the rubyNetcatCommand | ||
option in your prettier configuration. | ||
`); | ||
@@ -149,0 +151,0 @@ } |
@@ -13,3 +13,3 @@ const rubyPrinter = require("./ruby/printer"); | ||
* https://github.com/github/linguist/blob/master/lib/linguist/languages.yml | ||
* https://github.com/rubocop-hq/rubocop/blob/master/spec/rubocop/target_finder_spec.rb | ||
* https://github.com/rubocop/rubocop/blob/master/spec/rubocop/target_finder_spec.rb | ||
*/ | ||
@@ -16,0 +16,0 @@ |
@@ -11,8 +11,6 @@ const parseSync = require("../parser/parseSync"); | ||
const pragmaPattern = /#\s*@(prettier|format)/; | ||
// This function handles checking whether or not the source string has the | ||
// pragma for prettier. This is an optional workflow for incremental adoption. | ||
function hasPragma(text) { | ||
return pragmaPattern.test(text); | ||
return /^\s*#[^\S\n]*@(format|prettier)\s*(\n|$)/.test(text); | ||
} | ||
@@ -19,0 +17,0 @@ |
@@ -150,3 +150,3 @@ const { | ||
// look like all kinds of things, listed in the case statement below. | ||
function printType(path, { forceUnionParens = false } = {}) { | ||
function printType(path, { forceParens = false } = {}) { | ||
const node = path.getValue(); | ||
@@ -161,3 +161,9 @@ | ||
case "optional": | ||
return concat([path.call(printType, "type"), "?"]); | ||
return concat([ | ||
path.call( | ||
(typePath) => printType(typePath, { forceParens: true }), | ||
"type" | ||
), | ||
"?" | ||
]); | ||
case "tuple": | ||
@@ -178,3 +184,3 @@ // If we don't have any sub types, we explicitly need the space in between | ||
if (forceUnionParens || path.getParentNode().class === "intersection") { | ||
if (forceParens) { | ||
return concat(["(", doc, ")"]); | ||
@@ -185,4 +191,19 @@ } | ||
} | ||
case "intersection": | ||
return group(join(concat([line, "& "]), path.map(printType, "types"))); | ||
case "intersection": { | ||
const doc = group( | ||
join( | ||
concat([line, "& "]), | ||
path.map( | ||
(typePath) => printType(typePath, { forceParens: true }), | ||
"types" | ||
) | ||
) | ||
); | ||
if (forceParens) { | ||
return concat(["(", doc, ")"]); | ||
} | ||
return doc; | ||
} | ||
case "class_singleton": | ||
@@ -528,3 +549,3 @@ return concat(["singleton(", node.name, ")"]); | ||
path.call( | ||
(typePath) => printType(typePath, { forceUnionParens: true }), | ||
(typePath) => printType(typePath, { forceParens: true }), | ||
"type", | ||
@@ -617,5 +638,12 @@ "return_type" | ||
// This function handles adding the format pragma to a source string. This is an | ||
// optional workflow for incremental adoption. | ||
function insertPragma(text) { | ||
return `# @format\n${text}`; | ||
} | ||
module.exports = { | ||
print: printNode, | ||
hasPrettierIgnore | ||
hasPrettierIgnore, | ||
insertPragma | ||
}; |
@@ -111,8 +111,14 @@ const { | ||
const node = path.getValue(); | ||
const blockNode = node.body[1]; | ||
const parts = path.call(print, "body", 0); | ||
if (node.body[1]) { | ||
if (blockNode) { | ||
let blockDoc = path.call(print, "body", 1); | ||
if (node.body[1].comments) { | ||
if (!(blockNode.comments || []).some(({ leading }) => leading)) { | ||
// If we don't have any leading comments, we can just prepend the | ||
// operator. | ||
blockDoc = concat(["&", blockDoc]); | ||
} else if (Array.isArray(blockDoc[0])) { | ||
// If we have a method call like: | ||
@@ -127,6 +133,16 @@ // | ||
// before the comment. | ||
blockDoc.parts[2] = concat(["&", blockDoc.parts[2]]); | ||
// | ||
// In prettier >= 2.3.0, the comments are printed as an array before the | ||
// content. I don't love this kind of reflection, but it's the simplest | ||
// way at the moment to get this right. | ||
blockDoc = blockDoc[0].concat( | ||
concat(["&", blockDoc[1]]), | ||
blockDoc.slice(2) | ||
); | ||
} else { | ||
// If we don't have any comments, we can just prepend the operator | ||
blockDoc = concat(["&", blockDoc]); | ||
// In prettier < 2.3.0, the comments are printed as part of a concat, so | ||
// we can reflect on how many leading comments there are to determine | ||
// which doc node we should modify. | ||
const index = blockNode.comments.filter(({ leading }) => leading).length; | ||
blockDoc.parts[index] = concat(["&", blockDoc.parts[index]]); | ||
} | ||
@@ -141,6 +157,30 @@ | ||
function printArgsAddStar(path, opts, print) { | ||
const node = path.getValue(); | ||
const docs = path.map(print, "body"); | ||
let docs = []; | ||
if (node.body[1].comments) { | ||
path.each((argPath, argIndex) => { | ||
const doc = print(argPath); | ||
// If it's the first child, then it's an array of args, so we're going to | ||
// concat that onto the existing docs if there are any. | ||
if (argIndex === 0) { | ||
if (doc.length > 0) { | ||
docs = docs.concat(doc); | ||
} | ||
return; | ||
} | ||
// If it's after the splat, then it's an individual arg, so we're just going | ||
// to push it onto the array. | ||
if (argIndex !== 1) { | ||
docs.push(doc); | ||
return; | ||
} | ||
// If we don't have any leading comments, we can just prepend the operator. | ||
const argsNode = argPath.getValue(); | ||
if (!(argsNode.comments || []).some(({ leading }) => leading)) { | ||
docs.push(concat(["*", doc])); | ||
return; | ||
} | ||
// If we have an array like: | ||
@@ -153,18 +193,22 @@ // | ||
// | ||
// or if we have an array like: | ||
// then we need to make sure we don't accidentally prepend the operator | ||
// before the comment(s). | ||
// | ||
// [ | ||
// *values # comment | ||
// ] | ||
// | ||
// then we need to make sure we don't accidentally prepend the operator | ||
// before the comment. | ||
const index = node.body[1].comments.filter(({ leading }) => leading).length; | ||
docs[1].parts[index] = concat(["*", docs[1].parts[index]]); | ||
} else { | ||
// If we don't have any comments, we can just prepend the operator | ||
docs[1] = concat(["*", docs[1]]); | ||
} | ||
// In prettier >= 2.3.0, the comments are printed as an array before the | ||
// content. I don't love this kind of reflection, but it's the simplest way | ||
// at the moment to get this right. | ||
if (Array.isArray(doc[0])) { | ||
docs.push(doc[0].concat(concat(["*", doc[1]]), doc.slice(2))); | ||
return; | ||
} | ||
return docs[0].concat(docs[1]).concat(docs.slice(2)); | ||
// In prettier < 2.3.0, the comments are printed as part of a concat, so | ||
// we can reflect on how many leading comments there are to determine which | ||
// doc node we should modify. | ||
const index = argsNode.comments.filter(({ leading }) => leading).length; | ||
doc.parts[index] = concat(["*", doc.parts[index]]); | ||
docs = docs.concat(doc); | ||
}, "body"); | ||
return docs; | ||
} | ||
@@ -171,0 +215,0 @@ |
@@ -7,2 +7,3 @@ const { | ||
indent, | ||
join, | ||
softline | ||
@@ -137,3 +138,9 @@ } = require("../../prettier"); | ||
return concat([methodDoc, argsDoc]); | ||
// If there are already parentheses, then we can just use the doc that's | ||
// already printed. | ||
if (argNode.type == "arg_paren") { | ||
return concat([methodDoc, argsDoc]); | ||
} | ||
return concat([methodDoc, " ", join(", ", argsDoc), " "]); | ||
} | ||
@@ -140,0 +147,0 @@ |
@@ -193,54 +193,56 @@ const { | ||
// A normalized print function for both `if` and `unless` nodes. | ||
const printConditional = (keyword) => (path, { rubyModifier }, print) => { | ||
if (canTernary(path)) { | ||
let ternaryParts = [path.call(print, "body", 0), " ? "].concat( | ||
printTernaryClauses( | ||
keyword, | ||
path.call(print, "body", 1), | ||
path.call(print, "body", 2, "body", 0) | ||
) | ||
); | ||
const printConditional = | ||
(keyword) => | ||
(path, { rubyModifier }, print) => { | ||
if (canTernary(path)) { | ||
let ternaryParts = [path.call(print, "body", 0), " ? "].concat( | ||
printTernaryClauses( | ||
keyword, | ||
path.call(print, "body", 1), | ||
path.call(print, "body", 2, "body", 0) | ||
) | ||
); | ||
if (["binary", "call"].includes(path.getParentNode().type)) { | ||
ternaryParts = ["("].concat(ternaryParts).concat(")"); | ||
if (["binary", "call"].includes(path.getParentNode().type)) { | ||
ternaryParts = ["("].concat(ternaryParts).concat(")"); | ||
} | ||
return group( | ||
ifBreak(printWithAddition(keyword, path, print), concat(ternaryParts)) | ||
); | ||
} | ||
return group( | ||
ifBreak(printWithAddition(keyword, path, print), concat(ternaryParts)) | ||
); | ||
} | ||
const [predicate, statements, addition] = path.getValue().body; | ||
const [predicate, statements, addition] = path.getValue().body; | ||
// If there's an additional clause that wasn't matched earlier, we know we | ||
// can't go for the inline option. | ||
if (addition) { | ||
return group(printWithAddition(keyword, path, print, { breaking: true })); | ||
} | ||
// If there's an additional clause that wasn't matched earlier, we know we | ||
// can't go for the inline option. | ||
if (addition) { | ||
return group(printWithAddition(keyword, path, print, { breaking: true })); | ||
} | ||
// If the body of the conditional is empty, then we explicitly have to use the | ||
// block form. | ||
if (isEmptyStmts(statements)) { | ||
return concat([ | ||
`${keyword} `, | ||
align(keyword.length + 1, path.call(print, "body", 0)), | ||
concat([hardline, "end"]) | ||
]); | ||
} | ||
// If the body of the conditional is empty, then we explicitly have to use the | ||
// block form. | ||
if (isEmptyStmts(statements)) { | ||
return concat([ | ||
`${keyword} `, | ||
align(keyword.length + 1, path.call(print, "body", 0)), | ||
concat([hardline, "end"]) | ||
]); | ||
} | ||
// If the predicate of the conditional contains an assignment, then we can't | ||
// know for sure that it doesn't impact the body of the conditional, so we | ||
// have to default to the block form. | ||
if (containsAssignment(predicate)) { | ||
return concat([ | ||
`${keyword} `, | ||
align(keyword.length + 1, path.call(print, "body", 0)), | ||
indent(concat([hardline, path.call(print, "body", 1)])), | ||
concat([hardline, "end"]) | ||
]); | ||
} | ||
// If the predicate of the conditional contains an assignment, then we can't | ||
// know for sure that it doesn't impact the body of the conditional, so we | ||
// have to default to the block form. | ||
if (containsAssignment(predicate)) { | ||
return concat([ | ||
`${keyword} `, | ||
align(keyword.length + 1, path.call(print, "body", 0)), | ||
indent(concat([hardline, path.call(print, "body", 1)])), | ||
concat([hardline, "end"]) | ||
]); | ||
} | ||
return printSingle(keyword)(path, { rubyModifier }, print); | ||
}; | ||
return printSingle(keyword)(path, { rubyModifier }, print); | ||
}; | ||
module.exports = { | ||
@@ -247,0 +249,0 @@ else: (path, opts, print) => { |
@@ -59,14 +59,3 @@ const { | ||
case "dyna_symbol": { | ||
const { parts } = print(path); | ||
// We're going to slice off the starting colon character so that we can | ||
// move it to the end. If there are comments, then we're going to go | ||
// further into the printed doc nodes. | ||
if (parts[0] === ":") { | ||
parts.splice(0, 1); | ||
} else { | ||
parts[1].parts.splice(0, 1); | ||
} | ||
return concat(parts.concat(":")); | ||
return concat([print(path), ":"]); | ||
} | ||
@@ -78,6 +67,8 @@ } | ||
const node = path.getValue(); | ||
const doc = print(path); | ||
let doc = print(path); | ||
if (node.type === "@label") { | ||
return `:${doc.slice(0, doc.length - 1)} =>`; | ||
doc = concat([":", doc.slice(0, doc.length - 1)]); | ||
} else if (node.type === "dyna_symbol") { | ||
doc = concat([":", doc]); | ||
} | ||
@@ -84,0 +75,0 @@ |
@@ -20,11 +20,4 @@ const { | ||
function printParams(path, opts, print) { | ||
const [ | ||
reqs, | ||
optls, | ||
rest, | ||
post, | ||
kwargs, | ||
kwargRest, | ||
block | ||
] = path.getValue().body; | ||
const [reqs, optls, rest, post, kwargs, kwargRest, block] = | ||
path.getValue().body; | ||
let parts = []; | ||
@@ -31,0 +24,0 @@ |
@@ -85,9 +85,102 @@ const { | ||
function printPercentSDynaSymbol(path, opts, print) { | ||
const node = path.getValue(); | ||
const parts = []; | ||
// Push on the quote, which includes the opening character. | ||
parts.push(node.quote); | ||
path.each((childPath) => { | ||
const childNode = childPath.getValue(); | ||
if (childNode.type !== "@tstring_content") { | ||
// Here we are printing an embedded variable or expression. | ||
parts.push(print(childPath)); | ||
} else { | ||
// Here we are printing plain string content. | ||
parts.push(join(literalline, childNode.body.split("\n"))); | ||
} | ||
}, "body"); | ||
// Push on the closing character, which is the opposite of the third | ||
// character from the opening. | ||
parts.push(quotePairs[node.quote[2]]); | ||
return concat(parts); | ||
} | ||
// We don't actually want to print %s symbols, as they're much more rarely seen | ||
// in the wild. But we're going to be forced into it if it's a multi-line symbol | ||
// or if the quoting would get super complicated. | ||
function shouldPrintPercentSDynaSymbol(node) { | ||
// We shouldn't print a %s dyna symbol if it was not already that way in the | ||
// original source. | ||
if (node.quote[0] !== "%") { | ||
return false; | ||
} | ||
// Here we're going to check if there is a closing character, a new line, or a | ||
// quote in the content of the dyna symbol. If there is, then quoting could | ||
// get weird, so just bail out and stick to the original bounds in the source. | ||
const closing = quotePairs[node.quote[2]]; | ||
return node.body.some( | ||
(child) => | ||
child.type === "@tstring_content" && | ||
(child.body.includes("\n") || | ||
child.body.includes(closing) || | ||
child.body.includes("'") || | ||
child.body.includes('"')) | ||
); | ||
} | ||
// Prints a dynamic symbol. Assumes there's a quote property attached to the | ||
// node that will tell us which quote to use when printing. We're just going to | ||
// use whatever quote was provided. | ||
// | ||
// In the case of a plain dyna symbol, node.quote will be either :" or :' | ||
// For %s dyna symbols, node.quote will be %s[, %s(, %s{, or %s< | ||
function printDynaSymbol(path, opts, print) { | ||
const { quote } = path.getValue(); | ||
const node = path.getValue(); | ||
return concat([":", quote].concat(path.map(print, "body")).concat(quote)); | ||
if (shouldPrintPercentSDynaSymbol(node)) { | ||
return printPercentSDynaSymbol(path, opts, print); | ||
} | ||
const parts = []; | ||
let quote; | ||
if (isQuoteLocked(node)) { | ||
if (node.quote.startsWith("%")) { | ||
quote = opts.rubySingleQuote ? "'" : '"'; | ||
} else { | ||
quote = node.quote.slice(1); | ||
} | ||
} else { | ||
quote = opts.rubySingleQuote && isSingleQuotable(node) ? "'" : '"'; | ||
} | ||
parts.push(quote); | ||
path.each((childPath) => { | ||
const child = childPath.getValue(); | ||
if (child.type !== "@tstring_content") { | ||
parts.push(print(childPath)); | ||
} else { | ||
parts.push( | ||
join(literalline, normalizeQuotes(child.body, quote).split("\n")) | ||
); | ||
} | ||
}, "body"); | ||
parts.push(quote); | ||
// If we're inside of an assoc_new node as the key, then it will handle | ||
// printing the : on its own since it could change sides. | ||
const parentNode = path.getParentNode(); | ||
if (parentNode.type !== "assoc_new" || parentNode.body[0] !== node) { | ||
parts.unshift(":"); | ||
} | ||
return concat(parts); | ||
} | ||
@@ -94,0 +187,0 @@ |
@@ -11,8 +11,6 @@ const parseSync = require("../parser/parseSync"); | ||
const pragmaPattern = /#\s*@(prettier|format)/; | ||
// This function handles checking whether or not the source string has the | ||
// pragma for prettier. This is an optional workflow for incremental adoption. | ||
function hasPragma(text) { | ||
return pragmaPattern.test(text); | ||
return /^\s*#[^\S\n]*@(format|prettier)\s*(\n|$)/.test(text); | ||
} | ||
@@ -19,0 +17,0 @@ |
@@ -130,2 +130,10 @@ const { concat, trim } = require("../prettier"); | ||
// This function handles adding the format pragma to a source string. This is an | ||
// optional workflow for incremental adoption. | ||
function insertPragma(text) { | ||
const boundary = text.startsWith("#") ? "\n" : "\n\n"; | ||
return `# @format${boundary}${text}`; | ||
} | ||
module.exports = { | ||
@@ -138,3 +146,4 @@ embed, | ||
printComment, | ||
isBlockComment | ||
isBlockComment, | ||
insertPragma | ||
}; |
@@ -5,2 +5,3 @@ const needsParens = [ | ||
"assoc_new", | ||
"binary", | ||
"call", | ||
@@ -7,0 +8,0 @@ "massign", |
@@ -1,2 +0,9 @@ | ||
const skippable = ["array", "hash", "heredoc", "lambda", "regexp_literal"]; | ||
const skippable = [ | ||
"array", | ||
"dyna_symbol", | ||
"hash", | ||
"heredoc", | ||
"lambda", | ||
"regexp_literal" | ||
]; | ||
@@ -3,0 +10,0 @@ function skipAssignIndent(node) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
360957
4674
225
71