diff --git a/.eslintrc.yml b/.eslintrc.yml index a7135b083..d015ea638 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -5,7 +5,6 @@ env: extends: - eslint:recommended - prettier - - prettier/unicorn plugins: - import - unicorn @@ -18,16 +17,19 @@ rules: - as-needed curly: error dot-notation: error - eqeqeq: - - error - - always - - null: ignore + eqeqeq: error import/no-extraneous-dependencies: - error - devDependencies: ["tests*/**", "scripts/**"] import/order: error - no-else-return: error + no-else-return: + - error + - allowElseIf: false + no-implicit-coercion: error no-inner-declarations: error + no-restricted-syntax: + - error + - 'BinaryExpression[operator=/^[!=]==$/] > UnaryExpression.left[operator="!"]' no-unneeded-ternary: error no-useless-return: error no-unused-vars: @@ -74,7 +76,9 @@ rules: unicorn/no-useless-undefined: error unicorn/prefer-includes: error unicorn/prefer-number-properties: error + unicorn/prefer-optional-catch-binding: error unicorn/prefer-regexp-test: error + unicorn/prefer-spread: error unicorn/prefer-string-slice: error overrides: - files: @@ -113,12 +117,12 @@ overrides: prettier-internal-rules/better-parent-property-check-in-needs-parens: error - files: src/**/*.js rules: + prettier-internal-rules/flat-ast-path-call: error prettier-internal-rules/no-doc-builder-concat: error - prettier-internal-rules/prefer-fast-path-each: error + prettier-internal-rules/no-empty-flat-contents-for-if-break: error + prettier-internal-rules/prefer-ast-path-each: error + prettier-internal-rules/prefer-indent-if-break: error prettier-internal-rules/prefer-is-non-empty-array: error - - files: src/document/*.js - rules: - prettier-internal-rules/no-doc-builder-concat: off - files: - src/language-*/**/*.js rules: @@ -136,3 +140,4 @@ overrides: - "src/language-js/parser-babel.js" - "src/language-js/parser-meriyah.js" - "src/language-js/pragma.js" + - "src/language-js/parser/json.js" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..b79df98b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: 🤔 Support question + url: https://stackoverflow.com/questions/ask?tags=prettier + about: Issues are dedicated to development purposes. If you have questions, please use Stack Overflow. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 05c74b1a3..2fe37ab61 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,15 +3,20 @@ updates: - package-ecosystem: "npm" directory: "/" schedule: - interval: "daily" - open-pull-requests-limit: 20 + interval: weekly + day: sunday + time: "01:00" - package-ecosystem: "npm" directory: "/website" schedule: - interval: "daily" + interval: weekly + day: sunday + time: "01:00" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: weekly + day: sunday + time: "01:00" diff --git a/.github/workflows/dev-package-test.yml b/.github/workflows/dev-package-test.yml index b65f53b10..002a62248 100644 --- a/.github/workflows/dev-package-test.yml +++ b/.github/workflows/dev-package-test.yml @@ -33,7 +33,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 with: node-version: ${{ matrix.node }} diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index 25b9492a9..15aa797db 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -45,9 +45,12 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2.3.4 + # `codecov/codecov-action` require depth to be at least `2`, see #10219 + with: + fetch-depth: 2 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 with: node-version: ${{ matrix.node }} @@ -66,8 +69,6 @@ jobs: uses: codecov/codecov-action@v1.2.1 if: matrix.ENABLE_CODE_COVERAGE with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage/lcov.info fail_ci_if_error: true # #8073 test @@ -80,6 +81,4 @@ jobs: uses: codecov/codecov-action@v1.2.1 if: matrix.ENABLE_CODE_COVERAGE with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage/lcov.info fail_ci_if_error: true diff --git a/.github/workflows/eslint-rules.yml b/.github/workflows/eslint-rules.yml index 75b183a46..12e0f4d53 100644 --- a/.github/workflows/eslint-rules.yml +++ b/.github/workflows/eslint-rules.yml @@ -3,11 +3,11 @@ name: Internal_ESLint_Rules_Test on: push: paths: - - "scripts/tools/eslint-plugin-prettier-internal-rules" + - "scripts/tools/eslint-plugin-prettier-internal-rules/**" - ".github/workflows/eslint-rules.yml" pull_request: paths: - - "scripts/tools/eslint-plugin-prettier-internal-rules" + - "scripts/tools/eslint-plugin-prettier-internal-rules/**" - ".github/workflows/eslint-rules.yml" jobs: @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 - name: Install Dependencies run: yarn install --frozen-lockfile diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 2407a9d8e..8a266f223 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 - name: Install Dependencies run: yarn install --frozen-lockfile diff --git a/.github/workflows/mark-issue-duplicate.yml b/.github/workflows/mark-issue-duplicate.yml new file mode 100644 index 000000000..53e203c00 --- /dev/null +++ b/.github/workflows/mark-issue-duplicate.yml @@ -0,0 +1,18 @@ +name: Mark Issue Duplicate + +on: + issue_comment: + types: + - created + +jobs: + mark-duplicate: + runs-on: ubuntu-latest + if: ${{ !github.event.issue.pull_request && startsWith(github.event.comment.body, 'Duplicate of ') }} + steps: + - uses: actions-cool/issues-helper@v2.1.2 + with: + actions: "mark-duplicate" + token: ${{ secrets.GITHUB_TOKEN }} + duplicate-labels: "type:duplicate" + close-issue: true diff --git a/.github/workflows/prod-test.yml b/.github/workflows/prod-test.yml index 839b29140..d7a213f14 100644 --- a/.github/workflows/prod-test.yml +++ b/.github/workflows/prod-test.yml @@ -17,14 +17,14 @@ jobs: uses: actions/checkout@v2.3.4 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 - name: Install Dependencies run: yarn install --frozen-lockfile - name: Cache Build Results id: build-cache - uses: actions/cache@v2 + uses: actions/cache@v2.1.4 with: path: .cache key: v2-build-cache-${{ hashFiles('yarn.lock') }}-${{ hashFiles('scripts/build/**/*') }}-${{ github.ref }}- @@ -61,7 +61,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 - name: Install Dependencies run: yarn install --frozen-lockfile @@ -109,7 +109,7 @@ jobs: uses: actions/checkout@v2.3.4 - name: Setup Node.js - uses: actions/setup-node@v2.1.4 + uses: actions/setup-node@v2.1.5 with: node-version: ${{ matrix.node }} diff --git a/.github/workflows/support.yml b/.github/workflows/support.yml new file mode 100644 index 000000000..1caae5059 --- /dev/null +++ b/.github/workflows/support.yml @@ -0,0 +1,17 @@ +name: "Support requests" + +on: + issues: + types: [labeled, unlabeled, reopened] + +jobs: + support: + runs-on: ubuntu-latest + steps: + - uses: dessant/support-requests@v2 + with: + github-token: ${{ github.token }} + support-label: "type:question" + issue-comment: > + :wave: @{issue-author}, we use the issue tracker exclusively for development purposes. + For questions, please use [Stack Overflow](https://stackoverflow.com/questions/ask?tags=prettier). diff --git a/.gitignore b/.gitignore index cdee460be..824eebe1c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ package-lock.json .pnp.* .nyc_output /scripts/tools/eslint-plugin-prettier-internal-rules/node_modules +.devcontainer diff --git a/.prettierignore b/.prettierignore index a9e368637..a51105f52 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,3 +10,4 @@ coverage/ /website/static/lib/ /website/static/playground.js cspell.json +.nyc_output diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5c227dd7..7a140f1a3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,30 +4,76 @@ To get up and running, install the dependencies and run the tests: ```bash yarn -yarn lint:eslint yarn test ``` -Here's what you need to know about the tests: +## Tests -- The tests use [Jest snapshots](https://facebook.github.io/jest/docs/en/snapshot-testing.html). -- You can make changes and run `jest -u` (or `yarn test -u`) to update the snapshots. Then run `git diff` to take a look at what changed. Always update the snapshots when opening a PR. -- You can run `FULL_TEST=1 jest` for a more robust test run. - - "compare AST" That re-parses the output, and compares the new AST with the original one and makes sure they are semantically equivalent. - - "second format" That formats the output again, and checks that the second output is the same as the first. - - "EOL "\r\n"" That replace end of line with `\r\n`, and checks that the output is the same as the first. - - "EOL "\r"" That replace end of line with `\r`, and checks that the output is the same as the first. - - "BOM" That add BOM(`U+FEFF`) to text, and checks that the output is the same as the first, and keeps the BOM. -- Each test folder has a `jsfmt.spec.js` that runs the tests. For JavaScript files, generally you can just put `run_spec(__dirname, ["babel", "flow", "typescript"]);` there. This will verify that the output using each parser is the same. You can also pass options as the third argument, like this: `run_spec(__dirname, ["babel"], { trailingComma: "es5" });` -- `tests/flow-repo/` contains the Flow test suite, and is not supposed to be edited by hand. To update it, clone the Flow repo next to the Prettier repo and run: `node scripts/sync-flow-tests.js ../flow/tests/`. -- If you would like to debug prettier locally, you can either debug it in Node or the browser. The easiest way to debug it in Node, is to create a local test file with some example code you want formatted and either run it in an editor like VS Code or run it directly via `./bin/prettier.js `. The easiest way to debug it in the browser is to build Prettier's website locally (see `website/README.md`). +The tests use [Jest snapshots](https://facebook.github.io/jest/docs/en/snapshot-testing.html). You can make changes and run `jest -u` (or `yarn test -u`) to update the snapshots. Then run `git diff` to take a look at what changed. Always update the snapshots when opening a PR. -Run `yarn lint:eslint --fix` to automatically format files. +Each test directory in `tests` has a `jsfmt.spec.js` file that controls how exactly the rest of the files in the directory are used for tests. This file must contain one or more calls to the `run_spec` global function. For example, in directories with JavaScript formatting tests, `jsfmt.spec.js` generally looks like this: -If you can, take look at [commands.md](commands.md) and check out [Wadler's paper](http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) to understand how Prettier works. +```js +run_spec(__dirname, ["babel", "flow", "typescript"]); +``` + +This verifies that for each file in the directory, the output matches the snapshot and is the same for each listed parser. + +You can also pass options as the third argument: + +```js +run_spec(__dirname, ["babel"], { trailingComma: "es5" }); +``` + +Signature: + +```ts +function run_spec( + fixtures: + | string + | { + dirname: string; + snippets?: Array< + | string + | { code: string; name?: string; filename?: string; output?: string } + >; + }, + parsers: string[], + options?: PrettierOptions & { + errors: true | { [parserName: string]: true | string[] }; + } +): void; +``` + +Parameters: + +- **`fixtures`**: Must be set to `__dirname` or to an object of the shape `{ dirname: __dirname, ... }`. The object may have the `snippets` property to specify an array of extra input entries in addition to the files in the current directory. For each input entry (a file or a snippet), `run_spec` configures and runs a number of tests. The main check is that for a given input the output should match the snapshot (for snippets, the expected output can also be specified directly). [Additional checks](#deeper-testing) are controlled by options and environment variables. +- **`parsers`**: A list of parser names. The tests verify that the parsers in this list produce the same output. If the list includes `typescript`, then `babel-ts` is included implicitly. If the list includes `babel`, and the current directory is inside `tests/js`, then `espree` and `meriyah` are included implicitly. +- **`options`**: In addition to Prettier's formatting option, can contain the `errors` property to specify that it's expected that the formatting shouldn't be successful and an error should be thrown for all (`errors: true`) or some combinations of input entries and parsers. + +The implementation of `run_spec` can be found in [`tests_config/run_spec.js`](tests_config/run_spec.js). + +`tests/flow-repo/` contains the Flow test suite and is not supposed to be edited by hand. To update it, clone the Flow repo next to the Prettier repo and run: `node scripts/sync-flow-tests.js ../flow/tests/`. + +## Debugging + +To debug Prettier locally, you can either debug it in Node (recommended) or the browser. + +- The easiest way to debug it in Node is to create a local test file with some example code you want formatted and either run it in an editor like VS Code or run it directly via `./bin/prettier.js `. +- The easiest way to debug it in the browser is to build Prettier's website locally (see [`website/README.md`](website/README.md)). + +## Other + +The project uses ESLint for linting and Prettier for formatting. If your editor isn't set up to work with them, you can lint and format all files from the command line using `yarn fix`. + +After opening a PR, describe your changes in a file in the `changelog_unreleased` directory following the template [`changelog_unreleased/TEMPLATE.md`](changelog_unreleased/TEMPLATE.md) and commit this file to your PR. + +Take a look at [`commands.md`](commands.md) and, if you know Haskell, check out [Wadler's paper](http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf) to understand how Prettier works. If you want to know more about Prettier's GitHub labels, see the [Issue Labels](https://github.com/prettier/prettier/wiki/Issue-Labels) page on the Wiki. +# Advanced topics + ## Performance If you're contributing a performance improvement, the following Prettier CLI options can help: @@ -52,3 +98,18 @@ In the above commands: - `> /dev/null` ensures the formatted output is discarded. In addition to the options above, you can use [`node --prof` and `node --prof-process`](https://nodejs.org/en/docs/guides/simple-profiling/), as well as `node --trace-opt --trace-deopt`, to get more advanced performance insights. + +## Regression testing + +We have a cool tool for regression testing that runs on GitHub Actions. Have a look: https://github.com/prettier/prettier-regression-testing + +## Deeper testing + +You can run `FULL_TEST=1 jest` for a more robust test run, which includes the following additional checks: + +- **compare AST** - re-parses the output and makes sure the new AST is equivalent to the original one. +- **second format** - formats the output again and checks that the second output is the same as the first. +- **EOL '\r\n'** and **EOL '\r'** - check that replacing line endings with `\r\n` or `\r` in the input doesn't affect the output. +- **BOM** - checks that adding BOM (`U+FEFF`) to the input affects the output in only one way: the BOM is preserved. + +Usually there is no need to run these extra checks locally, since they're run on the CI anyway. diff --git a/changelog_unreleased/api/10122.md b/changelog_unreleased/api/10122.md new file mode 100644 index 000000000..5cc6e6219 --- /dev/null +++ b/changelog_unreleased/api/10122.md @@ -0,0 +1,34 @@ +#### Fix `lineSuffixBoundary` (#10122 by @thorn0) + +There was a bug in the implementation of the [`lineSuffixBoundary`][lsb] command that significantly limited its usefulness: the printer algorithm didn't correctly consider it a potential line break. Now that the bug has been fixed, we urge plugin authors to give this command another try and see if it can help them simplify printing of trailing comments. + +[lsb]: https://github.com/prettier/prettier/blob/main/commands.md#linesuffixboundary + + +```jsx +// Input +group([ + "let foo = [", + indent([ + softline, + [lineSuffixBoundary, "item1,"], + line, + [lineSuffixBoundary, "item2,", lineSuffix(" // comment")], + line, + [lineSuffixBoundary, "item3"], + ]), + softline, + "];", +]) + +// Prettier stable +let foo = [item1, item2, // comment + item3]; + +// Prettier main +let foo = [ + item1, + item2, // comment + item3 +]; +``` diff --git a/changelog_unreleased/api/10221.md b/changelog_unreleased/api/10221.md new file mode 100644 index 000000000..5e4fecb18 --- /dev/null +++ b/changelog_unreleased/api/10221.md @@ -0,0 +1,3 @@ +#### Add `indentIfBreak` IR command (#10221 by @thorn) + +`indentIfBreak(doc, { groupId })` is an optimized version of `ifBreak(indent(doc), doc, { groupId })`. diff --git a/changelog_unreleased/api/9733.md b/changelog_unreleased/api/9733.md index 3b4f8369b..4def4af71 100644 --- a/changelog_unreleased/api/9733.md +++ b/changelog_unreleased/api/9733.md @@ -4,6 +4,8 @@ To simplify the code of AST printers, the data structure for the concatenation c If you're a plugin author, this change should only concern you if your plugin introspects or modifies composed docs. If it happens to be the case, please make your plugin compatible with future versions of Prettier by tweaking the introspecting code to support the new format. There also is an off-chance where this change can break things, namely if a plugin calls another plugin to [print an embedded language](https://prettier.io/docs/en/plugins.html#optional-embed) and then introspects the returned doc. There seems to be no reason for plugins to do that though. +To replace `concat(…)` calls in your plugins, you can try auto-fix by this ESLint rule [`prettier-doc/no-concat`](https://github.com/fisker/eslint-plugin-prettier-doc#no-concat). + ```jsx // Prettier stable myDoc = group(concat(["foo", line, "bar"])); diff --git a/changelog_unreleased/cli/10124.md b/changelog_unreleased/cli/10124.md new file mode 100644 index 000000000..0c3f204e6 --- /dev/null +++ b/changelog_unreleased/cli/10124.md @@ -0,0 +1,3 @@ +#### Add CLI flag for debugging comment attachment issues (#10124 by @thorn0) + +A new `--debug-print-comments` CLI flag and corresponding functionality for the Playground. diff --git a/changelog_unreleased/cli/10169.md b/changelog_unreleased/cli/10169.md new file mode 100644 index 000000000..7a0b6249a --- /dev/null +++ b/changelog_unreleased/cli/10169.md @@ -0,0 +1,3 @@ +#### Round-trippable `--debug-print-doc` (#10169, #10177 by @thorn0) + +The idea is to make the output of `--debug-print-doc` closer to actual code for generating docs (Prettier's intermediate representation). Ideally, it should be possible for it to work without modification after copy-pasting into a JS file. That ideal hasn't probably been reached by this PR, but it's pretty close. This is going to make `--debug-print-doc` and the corresponding part of the Playground a bit more useful. diff --git a/changelog_unreleased/cli/10208.md b/changelog_unreleased/cli/10208.md new file mode 100644 index 000000000..c8dac1692 --- /dev/null +++ b/changelog_unreleased/cli/10208.md @@ -0,0 +1,11 @@ +#### Print an error message when `--find-config-path` can't find config file (#10208 by @fisker) + +```console +# Prettier stable +$ prettier --find-config-path /prettier.js +# Silently failed + +# Prettier main +$ prettier --find-config-path /prettier.js +[error] Can not find configure file for "/prettier.js" +``` diff --git a/changelog_unreleased/cli/10217.md b/changelog_unreleased/cli/10217.md new file mode 100644 index 000000000..aba596519 --- /dev/null +++ b/changelog_unreleased/cli/10217.md @@ -0,0 +1,17 @@ +#### Clear printed long filenames of formatting file (#10217 by @fisker) + + +```console +# Prettier stable +$ prettier tests/flow-repo/config_module_system_node_resolve_dirname --check +Checking formatting... +tests\flow-repo\config_module_system_node_resolve_dirname\custom_resolve_dir\tes +tests\flow-repo\config_module_system_node_resolve_dirname\custom_resolve_dir\tes +tests\flow-repo\config_module_system_node_resolve_dirname\subdir\custom_resolve_ +All matched files use Prettier code style! + +// Prettier main +$ prettier tests/flow-repo/config_module_system_node_resolve_dirname --check +Checking formatting... +All matched files use Prettier code style! +``` diff --git a/changelog_unreleased/cli/10256.md b/changelog_unreleased/cli/10256.md new file mode 100644 index 000000000..1ac6e1126 --- /dev/null +++ b/changelog_unreleased/cli/10256.md @@ -0,0 +1,14 @@ +#### Don't skip file paths containing special words like `constructor` (#10256 by @ashlkv) + +Directories whose names happened to coincide with the properties of `Object.prototype` were ignored by Prettier CLI because of a classic bug (introduced in Prettier 2.0.0) with object properties not being checked for being own. + + +```sh +# Prettier stable +$ prettier 'js/constructor/*.js' --write +[error] No matching files. Patterns: js/constructor/*.js + +# Prettier main +prettier 'js/constructor/*.js' --write +js/constructor/test.js 42ms +``` diff --git a/changelog_unreleased/cli/8233.md b/changelog_unreleased/cli/8233.md new file mode 100644 index 000000000..6a7fdae12 --- /dev/null +++ b/changelog_unreleased/cli/8233.md @@ -0,0 +1,16 @@ +#### Add CLI option to prevent errors on unmatched pattern (#8233 by @daronmcintosh) + +The following command will not throw an error when it doesn't find a YAML file. + +```sh +// Prettier stable +$ npx prettier --check "prettier/docs/*.yaml" +Checking formatting... +[error] No files matching the pattern were found: "prettier/docs/*.yaml". +All matched files use Prettier code style! + +// Prettier main +$ npx prettier --check --no-error-on-unmatched-pattern "prettier/docs/*.yaml" +Checking formatting... +All matched files use Prettier code style! +``` diff --git a/changelog_unreleased/flow/10397.md b/changelog_unreleased/flow/10397.md new file mode 100644 index 000000000..3df2ddcc3 --- /dev/null +++ b/changelog_unreleased/flow/10397.md @@ -0,0 +1,15 @@ +#### Support `this` type annotation in functions via `babel-flow` (#10397 by @sosukesuzuki) + +`this` type annotation is supported since [Babel 7.13](https://babeljs.io/blog/2021/02/22/7.13.0#new-flow-features-12193httpsgithubcombabelbabelpull12193-12234httpsgithubcombabelbabelpull12234). + +```js +// Input +var foo: (this: boolean) => void; + +// Prettier stable +SyntaxError: Unexpected token, expected ")" (1:15) + +// Prettier main +var foo: (this: boolean) => void; + +``` diff --git a/changelog_unreleased/handlebars/10145.md b/changelog_unreleased/handlebars/10145.md new file mode 100644 index 000000000..2ba7277f5 --- /dev/null +++ b/changelog_unreleased/handlebars/10145.md @@ -0,0 +1,25 @@ +#### Fix formatting of attributes (#10145 by @thorn0) + +- fix escaping of `{{` in attributes and text +- fix the choice between `'` and `"` for attributes with interpolations +- fix the bug with `[object Object]` printed in the `class` attribute +- implement simple formatting for the `class` attribute, like Prettier formatted it in HTML before v2.3.0 + + +```html +// Input +
+
+
+ +// Prettier stable +
+
+
+ +// Prettier main +
+
+
+``` diff --git a/changelog_unreleased/handlebars/10179.md b/changelog_unreleased/handlebars/10179.md new file mode 100644 index 000000000..ad2fb046b --- /dev/null +++ b/changelog_unreleased/handlebars/10179.md @@ -0,0 +1,20 @@ +#### Split text content on multiple lines (#10179 by @dcyriller) + + +```handlebars +{{!-- Input --}} +
+ A long enough string to trigger a line break that would prevent wrapping more and more. +
+ +{{!-- Prettier stable --}} +
+ A long enough string to trigger a line break that would prevent wrapping more and more. +
+ +{{!-- Prettier main --}} +
+ A long enough string to trigger a line break that would prevent wrapping more + and more. +
+``` diff --git a/changelog_unreleased/handlebars/9885.md b/changelog_unreleased/handlebars/9885.md new file mode 100644 index 000000000..2fe1c4e9b --- /dev/null +++ b/changelog_unreleased/handlebars/9885.md @@ -0,0 +1,45 @@ +#### Add a whitespace sensitive mode (#9885 by @dcyriller) + +```sh +--html-whitespace-sensitivity strict +``` + + +```hbs +{{!-- Input --}} +123 {{mustache}} + +123 {{mustache}} +123 {{mustache}} + + +123 {{mustache}} + + +{{!-- Prettier stable --}} + + 123 {{mustache}} + + + 123 {{mustache}} + + + 123 {{mustache}} + + + 123 {{mustache}} + + +{{!-- Prettier main --}} +123 {{mustache}} + + 123 + {{mustache}} +123 + {{mustache}} + + + 123 + {{mustache}} + +``` diff --git a/changelog_unreleased/javascript/10106.md b/changelog_unreleased/javascript/10106.md new file mode 100644 index 000000000..cacf58331 --- /dev/null +++ b/changelog_unreleased/javascript/10106.md @@ -0,0 +1,45 @@ +#### Concise formatting of number-only arrays (#10106, #10160 by @thorn0) + +While in general, Prettier avoids this kind of formatting because it's not diff-friendly, in this special case we decided that the benefits outweigh the risks. + +If at least one element has a trailing single-line (`// ...`) comment on the same line, the concise formatting isn't applied. On the other hand, single-line comments placed on separate lines don't have such an effect and – as well as empty lines – can be used for logical grouping. + + +```jsx +// Input +const lazyCatererNumbers = [1, 2, 4, 7, 11, 16, 22, 29, 37, 46, + +// n > 10 +56, 67, 79, 92, 106, 121, 137, 154, 172, 191, 211, 232, 254, 277, 301, 326, 352, 379, 407, 436, 466, +497, 529, 562, 596, 631, 667, 704, 742, 781, +// n > 40 +821, 862, 904, 947, 991, 1036, 1082, 1129, 1177, 1226, 1276, 1327, 1379]; + +// Prettier stable +const lazyCatererNumbers = [ + 1, + 2, + 4, + 7, + 11, + 16, + 22, + 29, + 37, + // ... ✂ a lot of lines ✂ ... + 1379, +]; + + +// Prettier main +const lazyCatererNumbers = [ + 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, + + // n > 10 + 56, 67, 79, 92, 106, 121, 137, 154, 172, 191, 211, 232, 254, 277, 301, 326, + 352, 379, 407, 436, 466, 497, 529, 562, 596, 631, 667, 704, 742, 781, + // n > 40 + 821, 862, 904, 947, 991, 1036, 1082, 1129, 1177, 1226, 1276, 1327, 1379, +]; + +``` diff --git a/changelog_unreleased/javascript/10158.md b/changelog_unreleased/javascript/10158.md new file mode 100644 index 000000000..90c989c15 --- /dev/null +++ b/changelog_unreleased/javascript/10158.md @@ -0,0 +1,15 @@ +#### Break assignment whose rhs is wrapped by an unary expression (#10158 by @sosukesuzuki) + + +```js +// Input +const loooooooooooooooooooooooooong = !looooooooooooooong.looooooooooooooong.loooooong; + +// Prettier stable +const loooooooooooooooooooooooooong = !looooooooooooooong.looooooooooooooong + .loooooong; + +// Prettier main +const loooooooooooooooooooooooooong = + !looooooooooooooong.looooooooooooooong.loooooong; +``` diff --git a/changelog_unreleased/javascript/10187.md b/changelog_unreleased/javascript/10187.md new file mode 100644 index 000000000..86db16462 --- /dev/null +++ b/changelog_unreleased/javascript/10187.md @@ -0,0 +1,67 @@ +#### Consistent indentations for conditional operators (#10187, #10266 by @sosukesuzuki) + + +```ts +// Input +const dotNotationMemberExpression = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +).TSESTree.BinaryExpression; +const computedMemberExpression = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +)[TSESTree.BinaryExpression]; +const callExpressionCallee = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +)(TSESTree.BinaryExpression); +const typeScriptAsExpression = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +) as TSESTree.BinaryExpression; + +// Prettier stable +const dotNotationMemberExpression = (callNode.parent?.type === +AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +).TSESTree.BinaryExpression; +const computedMemberExpression = (callNode.parent?.type === +AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent)[TSESTree.BinaryExpression]; +const callExpressionCallee = (callNode.parent?.type === + AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent)(TSESTree.BinaryExpression); +const typeScriptAsExpression = (callNode.parent?.type === +AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent) as TSESTree.BinaryExpression; + +// Prettier main +const dotNotationMemberExpression = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +).TSESTree.BinaryExpression; +const computedMemberExpression = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +)[TSESTree.BinaryExpression]; +const callExpressionCallee = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +)(TSESTree.BinaryExpression); +const typeScriptAsExpression = ( + callNode.parent?.type === AST_NODE_TYPES.ChainExpression + ? callNode.parent.parent + : callNode.parent +) as TSESTree.BinaryExpression; +``` diff --git a/changelog_unreleased/javascript/10222.md b/changelog_unreleased/javascript/10222.md new file mode 100644 index 000000000..d21c50f3f --- /dev/null +++ b/changelog_unreleased/javascript/10222.md @@ -0,0 +1,57 @@ +#### Format assignments more consistently (#10222 by @thorn0) + +Previously, Prettier had a lot of trouble with figuring out how to break lines in assignments. E.g., long right-hand sides often stayed unbroken. Not anymore. + + +```ts +// Input +aParticularlyLongAndObnoxiousNameForIllustrativePurposes = + anotherVeryLongNameForIllustrativePurposes; + +aParticularlyLongAndObnoxiousNameForIllustrativePurposes = + 'a very long string for illustrative purposes'.length; + +someReallyLongThingStoredInAMapWithAReallyBigName[pageletID] = + _someVariableThatWeAreCheckingForFalsiness + ? Date.now() - _someVariableThatWeAreCheckingForFalsiness + : 0; + +class x { + private readonly rawConfigFromFile$: BehaviorSubject = new BehaviorSubject(notRead); +} + +// Prettier stable +aParticularlyLongAndObnoxiousNameForIllustrativePurposes = anotherVeryLongNameForIllustrativePurposes; + +aParticularlyLongAndObnoxiousNameForIllustrativePurposes = "a very long string for illustrative purposes" + .length; + +someReallyLongThingStoredInAMapWithAReallyBigName[ + pageletID +] = _someVariableThatWeAreCheckingForFalsiness + ? Date.now() - _someVariableThatWeAreCheckingForFalsiness + : 0; + +class x { + private readonly rawConfigFromFile$: BehaviorSubject = new BehaviorSubject( + notRead + ); +} + +// Prettier main +aParticularlyLongAndObnoxiousNameForIllustrativePurposes = + anotherVeryLongNameForIllustrativePurposes; + +aParticularlyLongAndObnoxiousNameForIllustrativePurposes = + "a very long string for illustrative purposes".length; + +someReallyLongThingStoredInAMapWithAReallyBigName[pageletID] = + _someVariableThatWeAreCheckingForFalsiness + ? Date.now() - _someVariableThatWeAreCheckingForFalsiness + : 0; + +class x { + private readonly rawConfigFromFile$: BehaviorSubject = + new BehaviorSubject(notRead); +} +``` diff --git a/changelog_unreleased/javascript/10238.md b/changelog_unreleased/javascript/10238.md new file mode 100644 index 000000000..c0e1a8e8f --- /dev/null +++ b/changelog_unreleased/javascript/10238.md @@ -0,0 +1,22 @@ +#### Improve formatting for React Hooks call (#10238 by @sosukesuzuki) + + +```js +// Input +const { firstName, lastName } = useMemo( + () => parseFullName(fullName), + [fullName] +); + +// Prettier stable +const { firstName, lastName } = useMemo(() => parseFullName(fullName), [ + fullName, +]); + +// Prettier main +const { firstName, lastName } = useMemo( + () => parseFullName(fullName), + [fullName] +); + +``` diff --git a/changelog_unreleased/javascript/10278.md b/changelog_unreleased/javascript/10278.md new file mode 100644 index 000000000..c6e3505c6 --- /dev/null +++ b/changelog_unreleased/javascript/10278.md @@ -0,0 +1,15 @@ +#### Fix error thrown on expressions like `a(b => c => function (){})` (#10278 by @thorn0) + +Regression from v2.2.0. + + +```jsx +// Input +a(b => c => function (){}) + +// Prettier stable +TypeError: Cannot read property 'length' of undefined + +// Prettier main +a((b) => (c) => function () {}); +``` diff --git a/changelog_unreleased/javascript/10296.md b/changelog_unreleased/javascript/10296.md new file mode 100644 index 000000000..d3405b77c --- /dev/null +++ b/changelog_unreleased/javascript/10296.md @@ -0,0 +1,25 @@ +#### Improve formatting for inline decorators (#10296 by @fisker) + + +```jsx +// Input +class Foo { + @decorator([]) bar() {} + @decorator( + [] + ) baz() {} +} + +// Prettier stable +class Foo { + @decorator([]) bar() {} + @decorator([]) + baz() {} +} + +// Prettier main +class Foo { + @decorator([]) bar() {} + @decorator([]) baz() {} +} +``` diff --git a/changelog_unreleased/javascript/10322.md b/changelog_unreleased/javascript/10322.md new file mode 100644 index 000000000..16f35a7e4 --- /dev/null +++ b/changelog_unreleased/javascript/10322.md @@ -0,0 +1,15 @@ +#### Disable Babel's error recovery for invalid escaped characters in identifiers (#10322 by @thorn0) + + +```js +// Input +const \u{20} = 1 + +// Prettier stable +const = 1; + +// Prettier main +[error] stdin: SyntaxError: Invalid Unicode escape (1:7) +[error] > 1 | const \u{20} = 1 +[error] | ^ ^ +``` diff --git a/changelog_unreleased/javascript/10334.md b/changelog_unreleased/javascript/10334.md new file mode 100644 index 000000000..8d41c6fe8 --- /dev/null +++ b/changelog_unreleased/javascript/10334.md @@ -0,0 +1,22 @@ +#### Fix ASI protection for private fields (#10334 by @thorn0) + + +```jsx +// Input +class C { + #field = 'value'; + ["method"]() {} +} + +// Prettier stable (with --no-semi) +class C { + #field = "value" + ["method"]() {} +} + +// Prettier main (with --no-semi) +class C { + #field = "value"; + ["method"]() {} +} +``` diff --git a/changelog_unreleased/javascript/10335.md b/changelog_unreleased/javascript/10335.md new file mode 100644 index 000000000..3c8113b1d --- /dev/null +++ b/changelog_unreleased/javascript/10335.md @@ -0,0 +1,22 @@ +#### Prevent wrapping object properties with short keys (#10335 by @thorn0) + +Line breaks after short property names in object literals often look unnatural. Even when such a line break yields a line length benefit of 1 or 2 characters, it rarely looks justified. Prettier main avoids line breaks after property names shorter than `tabWidth + 3`. The "+3" part has been chosen rather arbitrarily. It may be revised upward in future versions, provided there is a really good rationale. + + +```js +// Input +const importantLink = { + url: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about" +}; + +// Prettier stable +const importantLink = { + url: + "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about" +}; + +// Prettier main +const importantLink = { + url: "https://prettier.io/docs/en/rationale.html#what-prettier-is-concerned-about" +}; +``` diff --git a/changelog_unreleased/javascript/10342.md b/changelog_unreleased/javascript/10342.md new file mode 100644 index 000000000..b93c95c32 --- /dev/null +++ b/changelog_unreleased/javascript/10342.md @@ -0,0 +1,34 @@ +#### Improve formatting for nested await expressions in heads of member and call expressions (#10342 by @thorn0) + +Even though Prettier tries to be helpful here, please don't write code like this. Have mercy upon your teammates and use intermediate variables. + + +```jsx +// Input +const getAccountCount = async () => + (await + (await ( + await focusOnSection(BOOKMARKED_PROJECTS_SECTION_NAME) + ).findItem("My bookmarks")).getChildren() + ).length + +// Prettier stable +const getAccountCount = async () => + ( + await ( + await (await focusOnSection(BOOKMARKED_PROJECTS_SECTION_NAME)).findItem( + "My bookmarks" + ) + ).getChildren() + ).length; + +// Prettier main +const getAccountCount = async () => + ( + await ( + await ( + await focusOnSection(BOOKMARKED_PROJECTS_SECTION_NAME) + ).findItem("My bookmarks") + ).getChildren() + ).length; +``` diff --git a/changelog_unreleased/json/10323.md b/changelog_unreleased/json/10323.md new file mode 100644 index 000000000..eab0505f6 --- /dev/null +++ b/changelog_unreleased/json/10323.md @@ -0,0 +1,21 @@ +#### Don't use smart quotes for JSON5 with `--quote-props=preserve` (#10323 by @thorn0) + +With the `quoteProps` option set to `preserve` and `singleQuotes` to `false` (default), double quotes are always used for printing strings, including situations like `"bla\"bla"`. This effectively allows using `--parser json5` for "JSON with comments and trailing commas". + + +```js +// Input +{ + "char": "\"", +} + +// Prettier stable +{ + "char": '"', +} + +// Prettier main +{ + "char": "\"", +} +``` diff --git a/changelog_unreleased/json/10346.md b/changelog_unreleased/json/10346.md new file mode 100644 index 000000000..8169c1514 --- /dev/null +++ b/changelog_unreleased/json/10346.md @@ -0,0 +1,17 @@ +#### Stricter JSON parser (#10346, #10443, #10456 by @fisker) + +Prettier internally uses a JavaScript expression parser to parse JSON. That's why the `json` and `json5` parsers used to be very forgiving and allowed all kinds of JavaScript expressions. Now they are much stricter, although some simple non-standard syntax is still allowed (e.g., [JSON6](https://github.com/d3x0r/JSON6) is supported, except for multiple minus signs: `----123`). + + +```js +// Input +[1, 2, 1 + 2] + +// Prettier stable +[1, 2, 1 + 2] + +// Prettier main +SyntaxError: BinaryExpression is not allowed in JSON. (1:8) +> 1 | [1, 2, 1 + 2] + | ^ +``` diff --git a/changelog_unreleased/json/10433.md b/changelog_unreleased/json/10433.md new file mode 100644 index 000000000..91a49bf26 --- /dev/null +++ b/changelog_unreleased/json/10433.md @@ -0,0 +1,17 @@ +#### Improve error message (#10433 by @fisker) + + +```js +// Input +{key: "foo" + "bar"} + +// Prettier stable (--parser=json-stringify) +SyntaxError: BinaryExpression is not allowed in JSON. (1:7) +> 1 | {key: "foo" + "bar"} + | ^ + +// Prettier main +SyntaxError: BinaryExpression is not allowed in JSON. (1:7) +> 1 | {key: "foo" + "bar"} + | ^^^^^^^^^^^^^ +``` diff --git a/changelog_unreleased/markdown/9878.md b/changelog_unreleased/markdown/9878.md new file mode 100644 index 000000000..409d2c4bb --- /dev/null +++ b/changelog_unreleased/markdown/9878.md @@ -0,0 +1,31 @@ +#### Support YAML document end marker in front matter (#9878 by @michaelbeaumont) + +Add the ability to delimit the end of front matter with `...`. + + +```markdown + +--- +title: Hello +slug: home +... + +Markdown + + +--- + +title: Hello +slug: home +... + +Markdown + + +--- +title: Hello +slug: home +... + +Markdown +``` diff --git a/changelog_unreleased/scss/10005.md b/changelog_unreleased/scss/10005.md new file mode 100644 index 000000000..17b441df8 --- /dev/null +++ b/changelog_unreleased/scss/10005.md @@ -0,0 +1,17 @@ +#### Fix maps with keys that are lists or maps (#10005 by @fisker) + + +```scss +// Input +$map: ( + ('my list'): 'hello world', +); + +// Prettier stable +TypeError: Cannot read property 'length' of undefined + +// Prettier main +$map: ( + ("my list"): "hello world", +); +``` diff --git a/changelog_unreleased/typescript/10024.md b/changelog_unreleased/typescript/10024.md new file mode 100644 index 000000000..eca023a3c --- /dev/null +++ b/changelog_unreleased/typescript/10024.md @@ -0,0 +1,25 @@ +#### Fix unnecessary line breaks in method type declaration parameters (#10024 by @sosukesuzuki, #10357 by @thorn0) + + +```js +// Input +type Foo = { + method(foo: "foo"): ` + ` +}; + +// Prettier stable +type Foo = { + method( + foo: "foo" + ): ` + `; +}; + +// Prettier main +type Foo = { + method(foo: "foo"): ` + `; +}; + +``` diff --git a/changelog_unreleased/typescript/10109.md b/changelog_unreleased/typescript/10109.md new file mode 100644 index 000000000..e1bbb5aa5 --- /dev/null +++ b/changelog_unreleased/typescript/10109.md @@ -0,0 +1,29 @@ +#### Print trailing commas in type parameters (#10109 by @sosukesuzuki, #10353 by @thorn0) + +TypeScript has been supporting trailing commas in type parameters since TypeScript 2.7 released in January 2018. Prettier main prints them if the `trailingComma` option is set to `all`. Keep this option at the more conservative default value `es5` if compatibility with TypeScript 2.7 or older is needed. Note that TypeScript [still doesn't support][ts-issue-21984] trailing commas in type arguments (type parameter instantiations). + +[ts-issue-21984]: https://github.com/microsoft/TypeScript/issues/21984 + + +```ts +// Input +export class BaseSingleLevelProfileTargeting< + T extends ValidSingleLevelProfileNode, +> { + // ... +} + +// Prettier stable +export class BaseSingleLevelProfileTargeting< + T extends ValidSingleLevelProfileNode +> { + // ... +} + +// Prettier main with --trailling-comma=all +export class BaseSingleLevelProfileTargeting< + T extends ValidSingleLevelProfileNode, +> { + // ... +} +``` diff --git a/changelog_unreleased/typescript/10316.md b/changelog_unreleased/typescript/10316.md new file mode 100644 index 000000000..4ceeae64d --- /dev/null +++ b/changelog_unreleased/typescript/10316.md @@ -0,0 +1,21 @@ +#### Allow hugging arguments that are non-concise arrow functions with return type annotations (#10316 by @thorn0) + + +```ts +// Input +users.map((user: User): User => { + return user; +}) + +// Prettier stable +users.map( + (user: User): User => { + return user; + } +); + +// Prettier main +users.map((user: User): User => { + return user; +}) +``` diff --git a/changelog_unreleased/typescript/10337.md b/changelog_unreleased/typescript/10337.md new file mode 100644 index 000000000..defd35cd3 --- /dev/null +++ b/changelog_unreleased/typescript/10337.md @@ -0,0 +1,27 @@ +#### Correct parentheses for non-null assertions (#10337 by @thorn0) + +Necessary parentheses sometimes weren't printed in expressions containing non-null assertions. This has been fixed. + + +```ts +// Input +const myFunction2 = (key: string): number => + ({ + a: 42, + b: 42, + }[key]!) + +// Prettier stable (invalid syntax) +const myFunction2 = (key: string): number => + { + a: 42, + b: 42, + }[key]!; + +// Prettier main +const myFunction2 = (key: string): number => + ({ + a: 42, + b: 42, + }[key]!); +``` diff --git a/changelog_unreleased/typescript/10341.md b/changelog_unreleased/typescript/10341.md new file mode 100644 index 000000000..91028c186 --- /dev/null +++ b/changelog_unreleased/typescript/10341.md @@ -0,0 +1,31 @@ +#### Indent type assertions in heads of member and call expressions (#10341 by @thorn0) + + +```ts +// Input +const getAccountCount = async () => + (await + ((await ( + await focusOnSection(BOOKMARKED_PROJECTS_SECTION_NAME) + ).findItem("My bookmarks")) as TreeItem + ).getChildren() + ).length + +// Prettier stable +const getAccountCount = async () => + ( + await ((await ( + await focusOnSection(BOOKMARKED_PROJECTS_SECTION_NAME) + ).findItem("My bookmarks")) as TreeItem).getChildren() + ).length; + +// Prettier main +const getAccountCount = async () => + ( + await ( + (await ( + await focusOnSection(BOOKMARKED_PROJECTS_SECTION_NAME) + ).findItem("My bookmarks")) as TreeItem + ).getChildren() + ).length; +``` diff --git a/changelog_unreleased/typescript/10390.md b/changelog_unreleased/typescript/10390.md new file mode 100644 index 000000000..0c4671a3e --- /dev/null +++ b/changelog_unreleased/typescript/10390.md @@ -0,0 +1,14 @@ +#### Support intrinsic keyword (#10390 by @sosukesuzuki) + + +```ts +// Input +type Uppercase = intrinsic; + +// Prettier stable +Error: unknown type: "TSIntrinsicKeyword" + +// Prettier main +type Uppercase = intrinsic; + +``` diff --git a/changelog_unreleased/typescript/10418.md b/changelog_unreleased/typescript/10418.md new file mode 100644 index 000000000..c7c8cfaf2 --- /dev/null +++ b/changelog_unreleased/typescript/10418.md @@ -0,0 +1,16 @@ +#### Support TypeScript 4.2 `abstract` Construct Signatures (#10418, #10466 by @sosukesuzuki) + +Support [`abstract` Construct Signatures](https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/#abstract-construct-signatures). + + +```ts +// Input +type T = abstract new () => void; + +// Prettier main +SyntaxError: Unexpected token, expected ";" (1:19) + +// Prettier stable +type T = abstract new () => void; + +``` diff --git a/changelog_unreleased/typescript/10457.md b/changelog_unreleased/typescript/10457.md new file mode 100644 index 000000000..8587da5a2 --- /dev/null +++ b/changelog_unreleased/typescript/10457.md @@ -0,0 +1,13 @@ +#### Fix misplaced comments in unions and intersections (#10457 by @thorn0) + + +```ts +// Input +type Foo = "1" | "2" /* two */ | "3"; + +// Prettier stable +type Foo = "1" | "2" | /* two */ "3"; + +// Prettier main +type Foo = "1" | "2" /* two */ | "3"; +``` diff --git a/commands.md b/commands.md index 0a0145418..28d32b4b4 100644 --- a/commands.md +++ b/commands.md @@ -1,20 +1,30 @@ -The core of the algorithm is implemented in `doc-{printer,builders,utils,debug}.js`. The printer should use the basic formatting abstractions provided to construct a format when printing a node. Parts of the API only exist to be compatible with recast's previous API to ease migration, but over time we can clean it up. +The core of the algorithm is implemented in `src/document/doc-{printer,builders,utils}.js`. The printer uses the basic formatting abstractions provided to construct a format when printing a node. -The following commands are available: +## Prettier's intermediate representation: `Doc` -### group +A doc can be a string, an array of docs, or a command. ```ts -type GroupOpts = { +type Doc = string | Doc[] | DocCommand; +``` + +- _strings_ are printed directly as is (however for the algorithm to work properly they shouldn't contain line break characters) +- _arrays_ are used to concatenate a list of docs to be printed sequentially into a single doc +- `DocCommand` is any of the following: + +### `group` + +```ts +type GroupOptions = { shouldBreak?: boolean; - expandedStates?: Doc[]; + id?: symbol; }; -declare function group(doc: Doc, opts?: GroupOpts): Doc; +declare function group(doc: Doc, options?: GroupOptions): Doc; ``` Mark a group of items which the printer should try to fit on one line. This is the basic command to tell the printer when to break. Groups are usually nested, and the printer will try to fit everything on one line, but if it doesn't fit it will break the outermost group first and try again. It will continue breaking groups until everything fits (or there are no more groups to break). -A document can force parent groups to break by including `breakParent` (see below). A hard and literal line automatically include this so they always break parent groups. Breaks are propagated to all parent groups, so if a deeply nested expression has a hard break, everything will break. This only matters for "hard" breaks, i.e. newlines that are printed no matter what and can be statically analyzed. +A group is forced to break if it's created with the `shouldBreak` option set to `true` or if it includes [`breakParent`](#breakParent). A [hard](#hardline) and [literal](#literalline) line breaks automatically include this so they always break parent groups. Breaks are propagated to all parent groups, so if a deeply nested expression has a hard break, everything will break. This only matters for "hard" breaks, i.e. newlines that are printed no matter what and can be statically analyzed. For example, an array will try to fit on one line: @@ -36,17 +46,16 @@ However, if any of the items inside the array have a hard break, the array will Functions always break after the opening curly brace no matter what, so the array breaks as well for consistent formatting. See [the implementation of `ArrayExpression`](#example) for an example. -### conditionalGroup +The `id` option can be used in [`ifBreak`](#ifBreak) checks. + +### `conditionalGroup` This should be used as **last resort** as it triggers an exponential complexity when nested. ```ts -type ConditionalGroupOpts = { - shouldBreak?: boolean; -}; declare function conditionalGroup( alternatives: Doc[], - opts?: ConditionalGroupOpts + options?: GroupOptions ): Doc; ``` @@ -56,89 +65,97 @@ This will try to print the first argument, if it fit use it, otherwise go to the conditionalGroup([a, b, c]); ``` -### fill +### `fill` ```ts declare function fill(docs: Doc[]): Doc; ``` -This is an alternative type of group which behaves like text layout: it's going to add a break whenever the next element doesn't fit in the line anymore. The difference with a typical group is that it's not going to break all the separators, just the ones that are at the end of lines. +This is an alternative type of group which behaves like text layout: it's going to add a break whenever the next element doesn't fit in the line anymore. The difference with [`group`](#group) is that it's not going to break all the separators, just the ones that are at the end of lines. ```js -fill(["I", line, "love", line, "prettier"]); +fill(["I", line, "love", line, "Prettier"]); ``` -### ifBreak +Expects the `docs` argument to be an array of alternating content and line breaks. In other words, elements with odd indices must be line breaks (e.g., [`softline`](#softline)). + +### `ifBreak` ```ts -declare function ifBreak(ifBreak: Doc, noBreak: Doc): Doc; +declare function ifBreak( + ifBreak: Doc, + noBreak?: Doc, + options?: { groupId?: symbol } +): Doc; ``` -Prints something if the current group breaks and something else if it doesn't. +Print something if the current `group` or the current element of `fill` breaks and something else if it doesn't. ```js ifBreak(";", " "); ``` -### breakParent +`groupId` can be used to check another _already printed_ group instead of the current group. + +### `breakParent` ```ts -declare var breakParent: Doc; +declare const breakParent: Doc; ``` -Include this anywhere to force all parent groups to break. See `group` for more info. Example: +Include this anywhere to force all parent groups to break. See [`group`](#group) for more info. Example: ```js group([" ", expr, " ", breakParent]); ``` -### join +### `join` ```ts declare function join(sep: Doc, docs: Doc[]): Doc; ``` -Join an array of items with a separator. +Join an array of docs with a separator. -### line +### `line` ```ts -declare var line: Doc; +declare const line: Doc; ``` Specify a line break. If an expression fits on one line, the line break will be replaced with a space. Line breaks always indent the next line with the current level of indentation. -### softline +### `softline` ```ts -declare var softline: Doc; +declare const softline: Doc; ``` Specify a line break. The difference from `line` is that if the expression fits on one line, it will be replaced with nothing. -### hardline +### `hardline` ```ts -declare var hardline: Doc; +declare const hardline: Doc; ``` Specify a line break that is **always** included in the output, no matter if the expression fits on one line or not. -### literalline +### `literalline` ```ts -declare var literalline: Doc; +declare const literalline: Doc; ``` -Specify a line break that is **always** included in the output, and don't indent the next line. This is used for template literals. +Specify a line break that is **always** included in the output and doesn't indent the next line. Also, unlike `hardline`, this kind of line break preserves trailing whitespace on the line it ends. This is used for template literals. -### lineSuffix +### `lineSuffix` ```ts declare function lineSuffix(suffix: Doc): Doc; ``` -This is used to implement trailing comments. In practice, it is not practical to find where the line ends and you don't want to accidentally print some code at the end of the comment. `lineSuffix` will buffer the output and flush it before any new line. +This is used to implement trailing comments. It's not practical to constantly check where the line ends to avoid accidentally printing some code at the end of a comment. `lineSuffix` buffers docs passed to it and flushes them before any new line. ```js ["a", lineSuffix(" // comment"), ";", hardline]; @@ -150,13 +167,13 @@ will output a; // comment ``` -### lineSuffixBoundary +### `lineSuffixBoundary` ```ts -declare var lineSuffixBoundary: Doc; +declare const lineSuffixBoundary: Doc; ``` -In cases where you embed code inside of templates, comments shouldn't be able to leave the code part. lineSuffixBoundary is an explicit marker you can use to flush code in addition to newlines. +In cases where you embed code inside of templates, comments shouldn't be able to leave the code part. `lineSuffixBoundary` is an explicit marker you can use to flush the [`lineSuffix`](#lineSuffix) buffer in addition to line breaks. ```js ["{", lineSuffix(" // comment"), lineSuffixBoundary, "}", hardline]; @@ -177,7 +194,7 @@ and **not** {} // comment ``` -### indent +### `indent` ```ts declare function indent(doc: Doc): Doc; @@ -185,7 +202,7 @@ declare function indent(doc: Doc): Doc; Increase the level of indentation. -### dedent +### `dedent` ```ts declare function dedent(doc: Doc): Doc; @@ -193,15 +210,15 @@ declare function dedent(doc: Doc): Doc; Decrease the level of indentation. (Each `align` is considered one level of indentation.) -### align +### `align` ```ts declare function align(n: number | string, doc: Doc): Doc; ``` -This is similar to indent but it increases the level of indentation by a fixed number or a string. -Trailing alignments in indentation are still spaces, but middle ones are transformed into one tab per `align` when `useTabs` enabled. -If it's using in a whitespace-sensitive language, e.g. markdown, you should use `n` with string value to force print it. +Increase the indentation by a fixed number of spaces or a string. A variant of [`indent`](#indent). + +When `useTabs` is enabled, trailing alignments in indentation are still spaces, but middle ones are transformed one tab per `align`. In a whitespace-sensitive context (e.g., Markdown), you should pass spaces to `align` as strings to prevent their replacement with tabs. For example: @@ -213,38 +230,93 @@ For example: - `` -> `<2 space>` - `` -> `<2 space>` -### markAsRoot +### `markAsRoot` ```ts declare function markAsRoot(doc: Doc): Doc; ``` -This marks the current indentation as root for `dedentToRoot` and `literalline`s. +Mark the current indentation as root for [`dedentToRoot`](#dedentToRoot) and [`literalline`](#literalline)s. -### dedentToRoot +### `dedentToRoot` ```ts declare function dedentToRoot(doc: Doc): Doc; ``` -This will dedent the current indentation to the root marked by `markAsRoot`. +Decrease the current indentation to the root marked by [`markAsRoot`](#markAsRoot). -### trim +### `trim` ```ts -declare var trim: Doc; +declare const trim: Doc; ``` -This will trim any whitespace or tab character on the current line. This is used for preprocessor directives. +Trim all the indentation on the current line. This can be used for preprocessor directives. Should be placed after a line break. -### cursor +### `indentIfBreak` + +_Added in v2.3.0_ ```ts -declare var cursor: Doc; +declare function indentIfBreak( + doc: Doc, + opts: { groupId: symbol; negate?: boolean } +): Doc; +``` + +An optimized version of `ifBreak(indent(doc), doc, { groupId })`. + +With `negate: true`, corresponds to `ifBreak(doc, indent(doc), { groupId })` + +It doesn't make sense to apply `indentIfBreak` to the current group because "indent if the current group is broken" is the normal behavior of `indent`. That's why `groupId` is required. + +### `label` + +_Added in v2.3.0_ + +```ts +declare function label(label: string, doc: Doc): Doc; +``` + +Mark a doc with a string label. This doesn't affect how the doc is printed, but can be useful for heuristics based on doc introspection. + +E.g., to decide how to print an assignment expression, we might want to know whether its right-hand side has been printed as a method call chain, not as a plain function call. If the method chain printing code uses `label` to mark its result, checking that condition can be as easy as `rightHandSideDoc.label === 'method-chain'`. + +### `hardlineWithoutBreakParent` and `literallineWithoutBreakParent` + +_Added in v2.3.0_ + +```ts +declare const hardlineWithoutBreakParent: Doc; +declare const literallineWithoutBreakParent: Doc; +``` + +These are used very rarely, for advanced formatting tricks. Unlike their "normal" counterparts, they don't include an implicit (`breakParent`)[#breakParent]. + +Examples: + +- `hardlineWithoutBreakParent` is used for printing tables in Prettier's Markdown printer. With `proseWrap` set to `never`, the columns are aligned only if none of the rows exceeds `printWidth`. +- `literallineWithoutBreakParent` is used in the [Ruby plugin](https://github.com/prettier/plugin-ruby) for [printing heredoc syntax](https://github.com/prettier/plugin-ruby/blob/b6e7bd6bc3f70de8f146aa58ad0c8310518bf467/src/ruby/nodes/heredocs.js). + +### `cursor` + +```ts +declare const cursor: Doc; ``` This is a placeholder value where the cursor is in the original input in order to find where it would be printed. +### [Deprecated] `concat` + +_This command has been deprecated in v2.3.0, use `Doc[]` instead_ + +```ts +declare function concat(docs: Doc[]): Doc; +``` + +Combine an array into a single doc. + ## Example For an example, here's the implementation of the `ArrayExpression` node type: @@ -270,13 +342,3 @@ group( ``` This is a group with opening and closing brackets, and possibly indented contents. Because it's a `group` it will always be broken up if any of the sub-expressions are broken. - -### [Deprecated] concat - -> This command is deprecated, use `Doc[]` instead. - -```ts -declare function concat(docs: Doc[]): Doc; -``` - -Combine an array into a single string. diff --git a/cspell.json b/cspell.json index 3dd55b8af..6fae2663d 100644 --- a/cspell.json +++ b/cspell.json @@ -106,6 +106,7 @@ "elektronik", "Eneman", "ENOENT", + "eqeqeq", "Ericsburgh", "errored", "Esben", @@ -272,6 +273,7 @@ "overscroll", "packagejson", "Panasenko", + "Pandoc", "Pangsakulyanont", "paren", "parens", @@ -369,6 +371,7 @@ "Tomasek", "Tradeshift", "Transloadit", + "trippable", "TSAs", "tsep", "TSESTree", @@ -409,6 +412,7 @@ "Vue's", "Wadler", "Wadler's", + "wcwidth", "webcompat", "webstorm", "Weixin", diff --git a/docs/cli.md b/docs/cli.md index 2e88f0fb9..86422e854 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -212,3 +212,7 @@ With `--ignore-unknown` (or `-u`), prettier will ignore unknown files matched by ```console $ prettier "**/*" --write --ignore-unknown ``` + +## `--no-error-on-unmatched-pattern` + +Prevent errors when pattern is unmatched. diff --git a/docs/ignore.md b/docs/ignore.md index ca0d9fb1c..1cca65e41 100644 --- a/docs/ignore.md +++ b/docs/ignore.md @@ -159,3 +159,13 @@ hello: world {{/my-crazy-component}} ``` + +## Command Line File Patterns + +For one-off commands, when you want to exclude some files without adding them to `.prettierignore`, negative patterns can come in handy: + +```bash +prettier --write . '!**/*.{js,jsx,vue}' +``` + +See [fast-glob](https://prettier.io/docs/en/cli.html#file-patterns) to learn more about advanced glob syntax. diff --git a/docs/options.md b/docs/options.md index 9a853bc98..9a1c04bd7 100644 --- a/docs/options.md +++ b/docs/options.md @@ -89,6 +89,15 @@ Valid options: | ------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------- | | `"as-needed"` | --quote-props | quoteProps: "" | +Note that Prettier never unquotes numeric property names in Angular expressions, TypeScript, and Flow because the distinction between string and numeric keys is significant in these languages. See: [Angular][quote-props-angular], [TypeScript][quote-props-typescript], [Flow][quote-props-flow]. Also Prettier doesn’t unquote numeric properties for Vue (see the [issue][quote-props-vue] about that). + +[quote-props-angular]: https://codesandbox.io/s/hungry-morse-foj87?file=/src/app/app.component.html +[quote-props-typescript]: https://www.typescriptlang.org/play?#code/DYUwLgBAhhC8EG8IEYBcKA0EBM7sQF8AoUSAIzkQgHJlr1ktrt6dCiiATEAY2CgBOICKWhR0AaxABPAPYAzCGGkAHEAugBuLr35CR4CGTKSZG5Wo1ltRKDHjHtQA +[quote-props-flow]: https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBjOA7AzgFzAA8wBeMAb1TDAAYAuMARlQF8g +[quote-props-vue]: https://github.com/prettier/prettier/issues/10127 + +If this option is set to `preserve`, `singleQuote` to `false` (default value), and `parser` to `json5`, double quotes are always used for strings. This effectively allows using the `json5` parser for “JSON with comments and trailing commas”. + ## JSX Quotes Use single quotes instead of double quotes in JSX. @@ -101,13 +110,13 @@ Use single quotes instead of double quotes in JSX. _Default value changed from `none` to `es5` in v2.0.0_ -Print trailing commas wherever possible when multi-line. (A single-line array, for example, never gets trailing commas.) +Print trailing commas wherever possible in multi-line comma-separated syntactic structures. (A single-line array, for example, never gets trailing commas.) Valid options: -- `"es5"` - Trailing commas where valid in ES5 (objects, arrays, etc.) +- `"es5"` - Trailing commas where valid in ES5 (objects, arrays, etc.). No trailing commas in type parameters in TypeScript. - `"none"` - No trailing commas. -- `"all"` - Trailing commas wherever possible (including [trailing commas in function parameter lists and calls](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas#Trailing_commas_in_functions)). This requires node 8 or a modern browser that supports ES2017 or transform with [babel](https://babeljs.io/docs/en/index). +- `"all"` - Trailing commas wherever possible (including [function parameters and calls](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Trailing_commas#Trailing_commas_in_functions)). To run, JavaScript code formatted this way needs an engine that supports ES2017 (Node.js 8+ or a modern browser) or [downlevel compilation](https://babeljs.io/docs/en/index). This also enables trailing commas in type parameters in TypeScript (supported since TypeScript 2.7 released in January 2018). | Default | CLI Override | API Override | | ------- | ------------------------------------------------------ | ------------------------------------------------------ | @@ -316,7 +325,7 @@ Specify the global whitespace sensitivity for HTML, Vue, Angular, and Handlebars Valid options: -- `"css"` - Respect the default value of CSS `display` property. For Handlebars treated same as `ignore`. +- `"css"` - Respect the default value of CSS `display` property. For Handlebars treated same as `strict`. - `"strict"` - Whitespace (or the lack of it) around all tags is considered significant. - `"ignore"` - Whitespace (or the lack of it) around all tags is considered insignificant. diff --git a/docs/plugins.md b/docs/plugins.md index 8795c1905..e7770bf13 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -49,25 +49,14 @@ Providing at least one path to `--plugin-search-dir`/`pluginSearchDirs` turns of - [`prettier-plugin-elm`](https://github.com/gicentre/prettier-plugin-elm) by [**@giCentre**](https://github.com/gicentre) - [`prettier-plugin-go-template`](https://github.com/NiklasPor/prettier-plugin-go-template) by [**@NiklasPor**](https://github.com/NiklasPor) - [`prettier-plugin-java`](https://github.com/jhipster/prettier-java) by [**@JHipster**](https://github.com/jhipster) -- [`prettier-plugin-jsdoc`](https://github.com/hosseinmd/prettier-plugin-jsdoc) by [**@hosseinmd**](https://github.com/hosseinmd) - [`prettier-plugin-kotlin`](https://github.com/Angry-Potato/prettier-plugin-kotlin) by [**@Angry-Potato**](https://github.com/Angry-Potato) -- [`prettier-plugin-package`](https://github.com/shellscape/prettier-plugin-package) by [**@shellscape**](https://github.com/shellscape) -- [`prettier-plugin-packagejson`](https://github.com/matzkoh/prettier-plugin-packagejson) by [**@matzkoh**](https://github.com/matzkoh) - [`prettier-plugin-pg`](https://github.com/benjie/prettier-plugin-pg) by [**@benjie**](https://github.com/benjie) - [`prettier-plugin-properties`](https://github.com/eemeli/prettier-plugin-properties) by [**@eemeli**](https://github.com/eemeli) - [`prettier-plugin-solidity`](https://github.com/prettier-solidity/prettier-plugin-solidity) by [**@mattiaerre**](https://github.com/mattiaerre) - [`prettier-plugin-svelte`](https://github.com/UnwrittenFun/prettier-plugin-svelte) by [**@UnwrittenFun**](https://github.com/UnwrittenFun) -- [`prettier-plugin-tailwind`](https://github.com/Acidic9/prettier-plugin-tailwind) by [**@Acidic9**](https://github.com/Acidic9) - [`prettier-plugin-toml`](https://github.com/bd82/toml-tools/tree/master/packages/prettier-plugin-toml) by [**@bd82**](https://github.com/bd82) -- [`prettier-plugin-pkg`](https://github.com/rx-ts/prettier/tree/master/packages/pkg) by [**@JounQin**](https://github.com/JounQin) - [`prettier-plugin-sh`](https://github.com/rx-ts/prettier/tree/master/packages/sh) by [**@JounQin**](https://github.com/JounQin) -## Experimental Plugins - -These plugins provide functionality not supported with Plugin API. Use with caution. - -- [`prettier-plugin-organize-imports`](https://github.com/simonhaenisch/prettier-plugin-organize-imports) by [**@simonhaenisch**](https://github.com/simonhaenisch) - ## Developing Plugins Prettier plugins are regular JavaScript modules with five exports: @@ -177,7 +166,7 @@ The printing process works as follows: 1. `preprocess(ast: AST, options: object): AST`, if available, is called. It is passed the AST from the _parser_. The AST returned by `preprocess` will be used by Prettier. If `preprocess` is not defined, the AST returned from the _parser_ will be used. 2. Comments are attached to the AST (see _Handling comments in a printer_ for details). -3. A Doc is recursively constructed from the AST. i) `embed(path: FastPath, print, textToDoc, options: object): Doc | null` is called on each AST node. If `embed` returns a Doc, that Doc is used. ii) If `embed` is undefined or returns a falsy value, `print(path: FastPath, options: object, print): Doc` is called on each AST node. +3. A Doc is recursively constructed from the AST. i) `embed(path: AstPath, print, textToDoc, options: object): Doc | null` is called on each AST node. If `embed` returns a Doc, that Doc is used. ii) If `embed` is undefined or returns a falsy value, `print(path: AstPath, options: object, print): Doc` is called on each AST node. #### `print` @@ -186,10 +175,10 @@ Most of the work of a plugin's printer will take place in its `print` function, ```ts function print( // Path to the AST node to print - path: FastPath, + path: AstPath, options: object, // Recursively print a child node - print: (path: FastPath) => Doc + print: (path: AstPath) => Doc ): Doc; ``` @@ -217,9 +206,9 @@ The `embed` function is called when the plugin needs to print one language insid ```ts function embed( // Path to the current AST node - path: FastPath, + path: AstPath, // Print a node with the current printer - print: (path: FastPath) => Doc, + print: (path: AstPath) => Doc, // Parse and print some text using a different parser. // You should set `options.parser` to specify which parser to use. textToDoc: (text: string, options: object) => Doc, @@ -271,7 +260,7 @@ Called whenever a comment node needs to be printed. It has the signature: ```ts function printComment( // Path to the current comment node - commentPath: FastPath, + commentPath: AstPath, // Current options options: object ): Doc; @@ -351,6 +340,8 @@ Nodes with comments are expected to have a `comments` property containing an arr The example above uses `util.addTrailingComment`, which automatically sets `comment.leading`/`trailing`/`printed` to appropriate values and adds the comment to the AST node's `comments` array. +The `--debug-print-comments` CLI flag can help with debugging comment attachment issues. It prints a detailed list of comments, which includes information on how every comment was classified (`ownLine`/`endOfLine`/`remaining`, `leading`/`trailing`/`dangling`) and to which node it was attached. For Prettier’s built-in languages, this information is also available on the Playground (the 'show comments' checkbox in the Debug section). + ### `options` `options` is an object containing the custom options your plugin supports. diff --git a/docs/related-projects.md b/docs/related-projects.md index 956232c3f..0f31a297f 100644 --- a/docs/related-projects.md +++ b/docs/related-projects.md @@ -28,6 +28,7 @@ title: Related Projects ## Misc +- [parallel-prettier](https://github.com/microsoft/parallel-prettier) is an alternative CLI that formats files in parallel to speed up large projects - [prettier_d](https://github.com/josephfrazier/prettier_d.js) runs Prettier as a server to avoid Node.js startup delay - [pretty-quick](https://github.com/azz/pretty-quick) formats changed files with Prettier - [rollup-plugin-prettier](https://github.com/mjeanroy/rollup-plugin-prettier) allows you to use Prettier with Rollup diff --git a/jest.config.js b/jest.config.js index 2ac2c033e..8dfc1a2bc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,16 +1,24 @@ "use strict"; +const path = require("path"); const installPrettier = require("./scripts/install-prettier"); +const PROJECT_ROOT = __dirname; const isProduction = process.env.NODE_ENV === "production"; -const ENABLE_CODE_COVERAGE = !!process.env.ENABLE_CODE_COVERAGE; -if (isProduction || process.env.INSTALL_PACKAGE) { - process.env.PRETTIER_DIR = installPrettier(); +const ENABLE_CODE_COVERAGE = Boolean(process.env.ENABLE_CODE_COVERAGE); +const TEST_STANDALONE = Boolean(process.env.TEST_STANDALONE); +const INSTALL_PACKAGE = Boolean(process.env.INSTALL_PACKAGE); + +let PRETTIER_DIR = isProduction + ? path.join(PROJECT_ROOT, "dist") + : PROJECT_ROOT; +if (INSTALL_PACKAGE || (isProduction && !TEST_STANDALONE)) { + PRETTIER_DIR = installPrettier(PRETTIER_DIR); } -const { TEST_STANDALONE } = process.env; +process.env.PRETTIER_DIR = PRETTIER_DIR; const testPathIgnorePatterns = []; -let transform; +let transform = {}; if (TEST_STANDALONE) { testPathIgnorePatterns.push("/tests_integration/"); } diff --git a/package.json b/package.json index 0b5187693..1030ec5c4 100644 --- a/package.json +++ b/package.json @@ -20,17 +20,17 @@ "bin" ], "dependencies": { - "@angular/compiler": "11.0.9", - "@babel/code-frame": "7.12.11", - "@babel/parser": "7.12.11", - "@glimmer/syntax": "0.73.1", + "@angular/compiler": "11.2.4", + "@babel/code-frame": "7.12.13", + "@babel/parser": "7.13.9", + "@glimmer/syntax": "0.77.1", "@iarna/toml": "2.2.5", - "@typescript-eslint/typescript-estree": "4.14.0", + "@typescript-eslint/typescript-estree": "4.16.1", "angular-estree-parser": "2.3.0", "angular-html-parser": "1.7.1", "camelcase": "6.2.0", "chalk": "4.1.0", - "ci-info": "watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540", + "ci-info": "3.1.1", "cjk-regex": "2.0.1", "cosmiconfig": "7.0.0", "dashify": "2.0.0", @@ -40,26 +40,26 @@ "escape-string-regexp": "4.0.0", "espree": "7.3.1", "esutils": "2.0.3", - "fast-glob": "3.2.4", + "fast-glob": "3.2.5", "fast-json-stable-stringify": "2.1.0", "find-parent-dir": "0.3.0", - "flow-parser": "0.143.1", + "flow-parser": "0.145.0", "get-stdin": "8.0.0", "globby": "11.0.2", - "graphql": "15.4.0", + "graphql": "15.5.0", "html-element-attributes": "2.3.0", "html-styles": "1.0.0", "html-tag-names": "1.1.5", "html-void-elements": "1.0.5", "ignore": "4.0.6", "jest-docblock": "26.0.0", - "json5": "2.1.3", + "json5": "2.2.0", "leven": "3.1.0", "lines-and-columns": "1.1.6", - "linguist-languages": "7.12.2", - "lodash": "4.17.20", + "linguist-languages": "7.13.0", + "lodash": "4.17.21", "mem": "8.0.0", - "meriyah": "4.0.0", + "meriyah": "4.1.5", "minimatch": "3.0.4", "minimist": "1.2.5", "n-readlines": "1.0.1", @@ -76,43 +76,44 @@ "remark-footnotes": "2.0.0", "remark-math": "3.0.1", "remark-parse": "8.0.3", - "resolve": "1.19.0", + "resolve": "1.20.0", "semver": "7.3.4", - "string-width": "4.2.0", - "typescript": "4.1.3", + "string-width": "4.2.2", + "strip-ansi": "6.0.0", + "typescript": "4.2.3", "unicode-regex": "3.0.0", - "unified": "9.2.0", + "unified": "9.2.1", "vnopts": "1.0.2", + "wcwidth": "1.0.1", "yaml-unist-parser": "1.3.1" }, "devDependencies": { - "@babel/core": "7.12.10", - "@babel/preset-env": "7.12.11", - "@babel/types": "7.12.12", - "@glimmer/reference": "0.73.2", - "@rollup/plugin-alias": "3.1.1", - "@rollup/plugin-babel": "5.2.2", + "@babel/core": "7.13.8", + "@babel/preset-env": "7.13.9", + "@babel/types": "7.13.0", + "@glimmer/reference": "0.77.1", + "@rollup/plugin-alias": "3.1.2", + "@rollup/plugin-babel": "5.3.0", "@rollup/plugin-commonjs": "16.0.0", "@rollup/plugin-json": "4.1.0", - "@rollup/plugin-node-resolve": "11.1.0", - "@rollup/plugin-replace": "2.3.4", + "@rollup/plugin-node-resolve": "11.2.0", + "@rollup/plugin-replace": "2.4.1", "@types/estree": "0.0.46", "@types/node": "14.14.0", - "@typescript-eslint/types": "4.14.0", "babel-jest": "26.6.3", "babel-loader": "8.2.2", "benchmark": "2.1.4", "builtin-modules": "3.2.0", "cross-env": "7.0.3", - "cspell": "4.2.5", - "eslint": "7.18.0", - "eslint-config-prettier": "7.2.0", + "cspell": "4.2.8", + "eslint": "7.21.0", + "eslint-config-prettier": "8.1.0", "eslint-formatter-friendly": "7.0.0", "eslint-plugin-import": "2.22.1", - "eslint-plugin-jest": "24.1.3", + "eslint-plugin-jest": "24.1.8", "eslint-plugin-prettier-internal-rules": "link:scripts/tools/eslint-plugin-prettier-internal-rules", "eslint-plugin-react": "7.22.0", - "eslint-plugin-unicorn": "27.0.0", + "eslint-plugin-unicorn": "28.0.2", "execa": "5.0.0", "jest": "26.6.3", "jest-snapshot-serializer-ansi": "1.0.0", @@ -121,14 +122,13 @@ "npm-run-all": "4.1.5", "path-browserify": "1.0.1", "prettier": "2.2.1", - "pretty-bytes": "5.5.0", + "pretty-bytes": "5.6.0", "rimraf": "3.0.2", - "rollup": "2.37.1", - "rollup-plugin-node-globals": "1.4.0", + "rollup": "2.40.0", + "rollup-plugin-polyfill-node": "0.6.1", "rollup-plugin-terser": "7.0.2", "shelljs": "0.8.4", "snapshot-diff": "0.8.1", - "strip-ansi": "6.0.0", "synchronous-promise": "2.0.15", "tempy": "1.0.0" }, @@ -148,7 +148,7 @@ "lint:eslint": "cross-env EFF_NO_LINK_RULES=true eslint . --format friendly", "lint:changelog": "node ./scripts/lint-changelog.js", "lint:prettier": "prettier . \"!test*\" --check", - "lint:dist": "eslint --no-eslintrc --no-ignore --env=es6,browser --parser-options=ecmaVersion:2018 \"dist/!(bin-prettier|index|third-party).js\"", + "lint:dist": "eslint --no-eslintrc --no-ignore --no-inline-config --env=es6,browser --parser-options=ecmaVersion:2018 \"dist/!(bin-prettier|index|third-party).js\"", "lint:spellcheck": "cspell \"**/*\" \".github/**/*\"", "lint:deps": "node ./scripts/check-deps.js", "fix": "run-s fix:eslint fix:prettier", diff --git a/scripts/build/build.js b/scripts/build/build.js index 0c4dccc34..886a1049e 100644 --- a/scripts/build/build.js +++ b/scripts/build/build.js @@ -97,9 +97,7 @@ async function createBundle(bundleConfig, cache, options) { const esmFile = path.join("dist/esm", output.replace(".js", ".mjs")); sizeTexts.push(`esm ${await getSizeText(esmFile)}`); } - process.stdout.write( - fitTerminal(output, sizeTexts.join(", ").concat(" ")) - ); + process.stdout.write(fitTerminal(output, `${sizeTexts.join(", ")} `)); } console.log(status.DONE); @@ -129,7 +127,7 @@ async function cacheFiles(cache) { path.join(".cache", file.replace("dist", "files")), ]); } - } catch (err) { + } catch { // Don't fail the build } } diff --git a/scripts/build/bundler.js b/scripts/build/bundler.js index 2e5420128..d886176a8 100644 --- a/scripts/build/bundler.js +++ b/scripts/build/bundler.js @@ -5,12 +5,11 @@ const { rollup } = require("rollup"); const { nodeResolve } = require("@rollup/plugin-node-resolve"); const rollupPluginAlias = require("@rollup/plugin-alias"); const commonjs = require("@rollup/plugin-commonjs"); -const nodeGlobals = require("rollup-plugin-node-globals"); +const rollupPluginPolyfillNode = require("rollup-plugin-polyfill-node"); const json = require("@rollup/plugin-json"); const replace = require("@rollup/plugin-replace"); const { terser } = require("rollup-plugin-terser"); const { babel } = require("@rollup/plugin-babel"); -const nativeShims = require("./rollup-plugins/native-shims"); const executable = require("./rollup-plugins/executable"); const evaluate = require("./rollup-plugins/evaluate"); const externals = require("./rollup-plugins/externals"); @@ -118,10 +117,13 @@ function getBabelConfig(bundle) { }, ], ]; - config.plugins.push([ - require.resolve("@babel/plugin-proposal-object-rest-spread"), - { loose: true, useBuiltIns: true }, - ]); + config.plugins.push( + [ + require.resolve("@babel/plugin-proposal-object-rest-spread"), + { loose: true, useBuiltIns: true }, + ], + require.resolve("@babel/plugin-proposal-optional-catch-binding") + ); return config; } @@ -137,7 +139,8 @@ function getRollupConfig(bundle) { // ignore `MIXED_EXPORTS` warn warning.code === "MIXED_EXPORTS" || (warning.code === "CIRCULAR_DEPENDENCY" && - warning.importer.startsWith("node_modules")) + (warning.importer.startsWith("node_modules") || + warning.importer.startsWith("polyfill-node:"))) ) { return; } @@ -190,8 +193,6 @@ function getRollupConfig(bundle) { evaluate(), json(), rollupPluginAlias(alias), - bundle.target === "universal" && - nativeShims(path.resolve(__dirname, "shims")), nodeResolve({ extensions: [".js", ".json"], preferBuiltins: bundle.target === "node", @@ -206,7 +207,7 @@ function getRollupConfig(bundle) { requireReturnsDefault: "preferred", }), externals(bundle.externals), - bundle.target === "universal" && nodeGlobals(), + bundle.target === "universal" && rollupPluginPolyfillNode(), babel(babelConfig), bundle.minify !== false && bundle.target === "universal" && diff --git a/scripts/build/cache.js b/scripts/build/cache.js index 8f7b5c961..ac37701ea 100644 --- a/scripts/build/cache.js +++ b/scripts/build/cache.js @@ -48,7 +48,7 @@ class Cache { for (const files of Object.values(this.files)) { assert.ok(Array.isArray(files)); } - } catch (err) { + } catch { this.checksums = {}; this.files = {}; } @@ -97,7 +97,7 @@ class Cache { async save() { try { await fs.writeFile(this.manifest, JSON.stringify(this.updated, null, 2)); - } catch (err) { + } catch { // Don't fail the build } } diff --git a/scripts/build/config.js b/scripts/build/config.js index c41ea56fb..e118a2636 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -44,6 +44,9 @@ const parsers = [ "extra.projects = prepareAndTransformProjects(": "extra.projects = [] || prepareAndTransformProjects(", "process.versions.node": "'999.999.999'", + // `rollup-plugin-polyfill-node` don't have polyfill for these modules + 'require("perf_hooks")': "{}", + 'require("inspector")': "{}", }, }, { @@ -156,7 +159,7 @@ function getFileOutput(bundle) { return bundle.output || path.basename(bundle.input); } -module.exports = coreBundles.concat(parsers).map((bundle) => ({ +module.exports = [...coreBundles, ...parsers].map((bundle) => ({ ...bundle, output: getFileOutput(bundle), })); diff --git a/scripts/build/rollup-plugins/native-shims.js b/scripts/build/rollup-plugins/native-shims.js deleted file mode 100644 index b05ac7d76..000000000 --- a/scripts/build/rollup-plugins/native-shims.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; - -const fs = require("fs"); -const path = require("path"); -const builtins = require("builtin-modules"); - -const EMPTY = "export default {};"; -const PREFIX = "\0shim:"; - -module.exports = function (dir) { - return { - resolveId(importee) { - if (importee.startsWith(PREFIX)) { - return importee; - } - - if (/\0/.test(importee) || !builtins.includes(importee)) { - return null; - } - - const shim = path.resolve(dir, importee + ".mjs"); - if (fs.existsSync(shim)) { - return shim; - } - return PREFIX + importee; - }, - - load(id) { - if (id.startsWith(PREFIX)) { - return EMPTY; - } - }, - }; -}; diff --git a/scripts/install-prettier.js b/scripts/install-prettier.js index ddd565f1f..d435defa6 100644 --- a/scripts/install-prettier.js +++ b/scripts/install-prettier.js @@ -6,12 +6,9 @@ const tempy = require("tempy"); shell.config.fatal = true; -const rootDir = path.join(__dirname, ".."); const client = process.env.NPM_CLIENT || "yarn"; -const packageDir = - process.env.NODE_ENV === "production" ? path.join(rootDir, "dist") : rootDir; -module.exports = () => { +module.exports = (packageDir) => { const tmpDir = tempy.directory(); const file = shell.exec("npm pack", { cwd: packageDir }).stdout.trim(); shell.mv(path.join(packageDir, file), tmpDir); diff --git a/scripts/release/utils.js b/scripts/release/utils.js index 22a8370e7..f19ff466c 100644 --- a/scripts/release/utils.js +++ b/scripts/release/utils.js @@ -34,10 +34,10 @@ function logPromise(name, promise) { } function runYarn(script) { - if (typeof script === "string") { - script = [script]; - } - return execa("yarn", ["--silent"].concat(script)).catch((error) => { + return execa("yarn", [ + "--silent", + ...(Array.isArray(script) ? script : [script]), + ]).catch((error) => { throw new Error(`\`yarn ${script}\` failed\n${error.stdout}`); }); } diff --git a/scripts/sync-flow-tests.js b/scripts/sync-flow-tests.js index 7705c78b8..68b0ef0d8 100644 --- a/scripts/sync-flow-tests.js +++ b/scripts/sync-flow-tests.js @@ -101,9 +101,9 @@ function run(argv) { "but that's not interesting for Prettier's tests.", "This is the skipped stuff:", "", - ] - .concat(skipped, "") - .join("\n") + ...skipped, + "", + ].join("\n") ); } diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js b/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js index 5a42c23e1..693cb0c37 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js @@ -49,7 +49,7 @@ module.exports = { type: "suggestion", docs: { url: - "https://github.com/prettier/prettier/blob/main/scripts/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js", + "https://github.com/prettier/prettier/blob/main/scripts/tools/eslint-plugin-prettier-internal-rules/better-parent-property-check-in-needs-parens.js", }, messages: { [MESSAGE_ID_PREFER_NAME_CHECK]: diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js b/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js new file mode 100644 index 000000000..b7a7ff986 --- /dev/null +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js @@ -0,0 +1,128 @@ +"use strict"; + +// This rule only work for nested `AstPath#call()` for now + +function astPathCallSelector(path) { + const prefix = path ? `${path}.` : ""; + return [ + `[${prefix}type="CallExpression"]`, + `[${prefix}callee.type="MemberExpression"]`, + `[${prefix}callee.property.type="Identifier"]`, + `[${prefix}callee.property.name="call"]`, + `[${prefix}arguments.length>1]`, + `[${prefix}arguments.0.type!="SpreadElement"]`, + ].join(""); +} + +// Matches: +// ``` +// path.call((childPath) => childPath.call(print, "b"), "a") +// ``` +const selector = [ + astPathCallSelector(), + '[arguments.0.type="ArrowFunctionExpression"]', + "[arguments.0.params.length=1]", + '[arguments.0.params.0.type="Identifier"]', + astPathCallSelector("arguments.0.body"), + '[arguments.0.body.callee.object.type="Identifier"]', +].join(""); + +const MESSAGE_ID = "flat-ast-path-call"; + +module.exports = { + meta: { + type: "suggestion", + docs: { + url: + "https://github.com/prettier/prettier/blob/main/scripts/tools/eslint-plugin-prettier-internal-rules/flat-ast-path-call.js", + }, + messages: { + [MESSAGE_ID]: "Do not use nested `AstPath#{{method}}(…)`.", + }, + fixable: "code", + }, + create(context) { + const sourceCode = context.getSourceCode(); + + return { + [selector](outerCall) { + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + const outerCallback = outerCall.arguments[0]; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^ + const outerCallbackParameterName = outerCallback.params[0].name; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^^^^^^^^^^^^^^^^^^ + const innerCall = outerCallback.body; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^^^^^ + const innerCallCalleeObjectName = innerCall.callee.object.name; + + if (outerCallbackParameterName !== innerCallCalleeObjectName) { + return; + } + + context.report({ + node: innerCall, + messageId: MESSAGE_ID, + data: { method: "call" }, + *fix(fixer) { + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^^^^^ + const innerCallback = innerCall.arguments[0]; + + yield fixer.replaceTextRange( + [ + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + outerCallback.range[0], + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + innerCallback.range[0], + ], + "" + ); + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + const innerNamesStart = innerCallback.range[1]; + + // path.call((childPath) => childPath.call(print, "b"), "a") + // ^ + const innerNamesEnd = innerCall.range[1] - 1; + + let innerNamesText = sourceCode.text.slice( + innerNamesStart, + innerNamesEnd + ); + + yield fixer.replaceTextRange( + [innerNamesStart, innerNamesEnd + 1], + "" + ); + + const [penultimateToken, lastToken] = sourceCode.getLastTokens( + outerCall, + 2 + ); + + // `outer` call has `trailing comma` + if ( + penultimateToken && + penultimateToken.type === "Punctuator" && + penultimateToken.value === "," + ) { + innerNamesText = innerNamesText.slice(1); + } + + yield fixer.insertTextBefore(lastToken, innerNamesText); + }, + }); + }, + }; + }, +}; diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/index.js b/scripts/tools/eslint-plugin-prettier-internal-rules/index.js index 3bec6a3e2..751ac8fa4 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/index.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/index.js @@ -4,10 +4,13 @@ module.exports = { rules: { "better-parent-property-check-in-needs-parens": require("./better-parent-property-check-in-needs-parens"), "directly-loc-start-end": require("./directly-loc-start-end"), + "flat-ast-path-call": require("./flat-ast-path-call"), "jsx-identifier-case": require("./jsx-identifier-case"), "no-doc-builder-concat": require("./no-doc-builder-concat"), + "no-empty-flat-contents-for-if-break": require("./no-empty-flat-contents-for-if-break"), "no-node-comments": require("./no-node-comments"), - "prefer-fast-path-each": require("./prefer-fast-path-each"), + "prefer-ast-path-each": require("./prefer-ast-path-each"), + "prefer-indent-if-break": require("./prefer-indent-if-break"), "prefer-is-non-empty-array": require("./prefer-is-non-empty-array"), "require-json-extensions": require("./require-json-extensions"), }, diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/no-empty-flat-contents-for-if-break.js b/scripts/tools/eslint-plugin-prettier-internal-rules/no-empty-flat-contents-for-if-break.js new file mode 100644 index 000000000..5d9d852e0 --- /dev/null +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/no-empty-flat-contents-for-if-break.js @@ -0,0 +1,42 @@ +"use strict"; + +const selector = [ + "CallExpression", + "[optional=false]", + '[callee.type="Identifier"]', + '[callee.name="ifBreak"]', + "[arguments.length=2]", + '[arguments.1.type="Literal"]', + '[arguments.1.value=""]', + ':not([arguments.0.type="SpreadElement"])', +].join(""); + +const messageId = "no-empty-flat-contents-for-if-break"; + +module.exports = { + meta: { + type: "suggestion", + docs: { + url: + "https://github.com/prettier/prettier/blob/main/scripts/eslint-plugin-prettier-internal-rules/no-empty-flat-contents-for-if-break.js", + }, + messages: { + [messageId]: + "Please don't pass an empty string to second parameter of ifBreak.", + }, + fixable: "code", + }, + create(context) { + return { + [selector](node) { + const [breakContents] = node.arguments; + context.report({ + node, + messageId, + fix: (fixer) => + fixer.removeRange([breakContents.range[1], node.range[1] - 1]), + }); + }, + }; + }, +}; diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-fast-path-each.js b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-ast-path-each.js similarity index 86% rename from scripts/tools/eslint-plugin-prettier-internal-rules/prefer-fast-path-each.js rename to scripts/tools/eslint-plugin-prettier-internal-rules/prefer-ast-path-each.js index 12bd32ed4..5cffcc174 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-fast-path-each.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-ast-path-each.js @@ -14,7 +14,7 @@ const selector = [ '[name="map"]', ].join(""); -const messageId = "prefer-fast-path-each"; +const messageId = "prefer-ast-path-each"; module.exports = { meta: { @@ -24,7 +24,7 @@ module.exports = { "https://github.com/prettier/prettier/blob/main/scripts/eslint-plugin-prettier-internal-rules/require-json-extensions.js", }, messages: { - [messageId]: "Prefer `FastPath#each()` over `FastPath#map()`.", + [messageId]: "Prefer `AstPath#each()` over `AstPath#map()`.", }, fixable: "code", }, diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-indent-if-break.js b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-indent-if-break.js new file mode 100644 index 000000000..99673835d --- /dev/null +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-indent-if-break.js @@ -0,0 +1,67 @@ +"use strict"; + +const selector = [ + "CallExpression", + "[optional=false]", + '[callee.type="Identifier"]', + '[callee.name="ifBreak"]', + "[arguments.length=3]", + '[arguments.0.type="CallExpression"]', + "[arguments.0.optional=false]", + '[arguments.0.callee.type="Identifier"]', + '[arguments.0.callee.name="indent"]', + "[arguments.0.arguments.length=1]", + '[arguments.0.arguments.0.type!="SpreadElement"]', + '[arguments.1.type!="SpreadElement"]', + '[arguments.2.type!="SpreadElement"]', +].join(""); + +const messageId = "prefer-indent-if-break"; + +module.exports = { + meta: { + type: "suggestion", + docs: { + url: + "https://github.com/prettier/prettier/blob/main/scripts/eslint-plugin-prettier-internal-rules/prefer-indent-if-break.js", + }, + messages: { + [messageId]: "Prefer `indentIfBreak(…)` over `ifBreak(indent(…), …)`.", + }, + fixable: "code", + }, + create(context) { + const sourceCode = context.getSourceCode(); + + return { + [selector](node) { + const indentDoc = node.arguments[0].arguments[0]; + const doc = node.arguments[1]; + + // Use text to compare same `doc` + if (sourceCode.getText(indentDoc) !== sourceCode.getText(doc)) { + return; + } + + context.report({ + node, + messageId, + *fix(fixer) { + yield fixer.replaceText(node.callee, "indentIfBreak"); + const openingParenthesisToken = sourceCode.getTokenAfter( + node.callee + ); + const commaToken = sourceCode.getTokenBefore( + doc, + ({ type, value }) => type === "Punctuator" && value === "," + ); + yield fixer.replaceTextRange( + [openingParenthesisToken.range[1], commaToken.range[1]], + "" + ); + }, + }); + }, + }; + }, +}; diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js index 68bdb80c4..e14c4ad56 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/prefer-is-non-empty-array.js @@ -112,7 +112,7 @@ module.exports = { left = left.right; } - if (!left.type === "UnaryExpression" || left.operator !== "!") { + if (left.type !== "UnaryExpression" || left.operator !== "!") { return; } diff --git a/scripts/tools/eslint-plugin-prettier-internal-rules/test.js b/scripts/tools/eslint-plugin-prettier-internal-rules/test.js index e02c8008e..ec8410669 100644 --- a/scripts/tools/eslint-plugin-prettier-internal-rules/test.js +++ b/scripts/tools/eslint-plugin-prettier-internal-rules/test.js @@ -73,6 +73,33 @@ test("directly-loc-start-end", { ], }); +test("flat-ast-path-call", { + valid: [ + 'path.call((childPath) => childPath.notCall(print, "b"), "a")', + 'path.notCall((childPath) => childPath.call(print, "b"), "a")', + 'path.call((childPath) => childPath.call(print, "b"))', + 'path.call((childPath) => childPath.call(print), "a")', + 'path.call((childPath) => notChildPath.call(print), "a")', + 'path.call(functionReference, "a")', + // Only check `arrow function` + 'path.call((childPath) => {return childPath.call(print, "b")}, "a")', + 'path.call(function(childPath) {return childPath.call(print, "b")}, "a")', + ], + invalid: [ + { + code: 'path.call((childPath) => childPath.call(print, "b"), "a")', + output: 'path.call(print, "a", "b")', + errors: [{ message: "Do not use nested `AstPath#call(…)`." }], + }, + { + // Trailing comma + code: 'path.call((childPath) => childPath.call(print, "b"), "a",)', + output: 'path.call(print, "a", "b")', + errors: 1, + }, + ], +}); + test("jsx-identifier-case", { valid: [ { @@ -144,7 +171,7 @@ test("no-node-comments", { ], }); -test("prefer-fast-path-each", { +test("prefer-ast-path-each", { valid: ["const foo = path.map()"], invalid: [ { @@ -155,6 +182,35 @@ test("prefer-fast-path-each", { ], }); +test("prefer-indent-if-break", { + valid: [ + "ifBreak(indent(doc))", + "notIfBreak(indent(doc), doc, options)", + "ifBreak(indent(doc), doc, )", + "ifBreak(...a, ...b, ...c)", + "ifBreak(notIndent(doc), doc, options)", + "ifBreak(indent(doc), notSameDoc, options)", + "ifBreak(indent(...a), a, options)", + "ifBreak(indent(a, b), a, options)", + ], + invalid: [ + { + code: "ifBreak(indent(doc), doc, options)", + output: "indentIfBreak( doc, options)", + errors: [ + { + message: "Prefer `indentIfBreak(…)` over `ifBreak(indent(…), …)`.", + }, + ], + }, + { + code: "ifBreak((indent(doc)), (doc), options)", + output: "indentIfBreak( (doc), options)", + errors: 1, + }, + ], +}); + test("prefer-is-non-empty-array", { valid: [ // `isNonEmptyArray` self is ignored @@ -204,3 +260,46 @@ test("require-json-extensions", { }, ], }); + +test("no-empty-flat-contents-for-if-break", { + valid: [ + "ifBreak('foo', 'bar')", + "ifBreak(doc1, doc2)", + "ifBreak(',')", + "ifBreak(doc)", + "ifBreak('foo', '', { groupId })", + "ifBreak(...foo, { groupId })", + ], + invalid: [ + { + code: "ifBreak('foo', '')", + output: "ifBreak('foo')", + errors: [ + { + message: + "Please don't pass an empty string to second parameter of ifBreak.", + }, + ], + }, + { + code: "ifBreak('foo' , '' )", + output: "ifBreak('foo')", + errors: [ + { + message: + "Please don't pass an empty string to second parameter of ifBreak.", + }, + ], + }, + { + code: "ifBreak(doc, '')", + output: "ifBreak(doc)", + errors: [ + { + message: + "Please don't pass an empty string to second parameter of ifBreak.", + }, + ], + }, + ], +}); diff --git a/src/cli/constant.js b/src/cli/constant.js index ff4321d9b..52758d2e9 100644 --- a/src/cli/constant.js +++ b/src/cli/constant.js @@ -132,6 +132,9 @@ const options = { "debug-print-doc": { type: "boolean", }, + "debug-print-comments": { + type: "boolean", + }, "debug-repeat": { // Repeat the formatting a few times and measure the average duration. type: "int", @@ -145,6 +148,10 @@ const options = { "Don't take .editorconfig into account when parsing configuration.", default: true, }, + "error-on-unmatched-pattern": { + type: "boolean", + oppositeDescription: "Prevent errors when pattern is unmatched.", + }, "find-config-path": { type: "path", category: coreOptions.CATEGORY_CONFIG, diff --git a/src/cli/context.js b/src/cli/context.js index 898946b06..7b88311ea 100644 --- a/src/cli/context.js +++ b/src/cli/context.js @@ -4,10 +4,9 @@ const pick = require("lodash/pick"); // eslint-disable-next-line no-restricted-modules const prettier = require("../index"); -const { createLogger } = require("./logger"); const { optionsModule, - optionsNormalizer, + optionsNormalizer: { normalizeCliOptions }, utils: { arrayify }, } = require("./prettier-internal"); const minimist = require("./minimist"); @@ -21,7 +20,7 @@ const createMinimistOptions = require("./create-minimist-options"); /** * @typedef {Object} Context * @property logger - * @property {string[]} args + * @property {string[]} rawArguments * @property argv * @property {string[]} filePatterns * @property {any[]} supportOptions @@ -30,24 +29,29 @@ const createMinimistOptions = require("./create-minimist-options"); * @property apiDefaultOptions * @property languages * @property {Partial[]} stack - * @property initContext * @property pushContextPlugins * @property popContextPlugins */ class Context { - constructor(args) { - this.args = args; + constructor({ rawArguments, logger }) { + this.rawArguments = rawArguments; + this.logger = logger; this.stack = []; - this._updateContextArgv(); - this._normalizeContextArgv(["loglevel", "plugin", "plugin-search-dir"]); - this.logger = createLogger(this.argv.loglevel); - this._updateContextArgv(this.argv.plugin, this.argv["plugin-search-dir"]); - } - initContext() { - // split into 2 step so that we could wrap this in a `try..catch` in cli/index.js - this._normalizeContextArgv(); + const { + plugin: plugins, + "plugin-search-dir": pluginSearchDirs, + } = parseArgvWithoutPlugins(rawArguments, logger, [ + "plugin", + "plugin-search-dir", + ]); + + this.pushContextPlugins(plugins, pluginSearchDirs); + + const argv = parseArgv(rawArguments, this.detailedOptions, logger); + this.argv = argv; + this.filePatterns = argv._.map((file) => String(file)); } /** @@ -64,67 +68,70 @@ class Context { "languages", ]) ); - this._updateContextOptions(plugins, pluginSearchDirs); + + Object.assign(this, getContextOptions(plugins, pluginSearchDirs)); } popContextPlugins() { Object.assign(this, this.stack.pop()); } - - _updateContextArgv(plugins, pluginSearchDirs) { - this.pushContextPlugins(plugins, pluginSearchDirs); - const minimistOptions = createMinimistOptions(this.detailedOptions); - const argv = minimist(this.args, minimistOptions); - this.argv = argv; - this.filePatterns = argv._.map((file) => String(file)); - } - - _normalizeContextArgv(keys) { - const detailedOptions = !keys - ? this.detailedOptions - : this.detailedOptions.filter((option) => keys.includes(option.name)); - const argv = !keys ? this.argv : pick(this.argv, keys); - this.argv = optionsNormalizer.normalizeCliOptions(argv, detailedOptions, { - logger: this.logger, - }); - } - - /** - * @param {string[]} plugins - * @param {string[]=} pluginSearchDirs - */ - _updateContextOptions(plugins, pluginSearchDirs) { - const { options: supportOptions, languages } = prettier.getSupportInfo({ - showDeprecated: true, - showUnreleased: true, - showInternal: true, - plugins, - pluginSearchDirs, - }); - const detailedOptionMap = normalizeDetailedOptionMap({ - ...createDetailedOptionMap(supportOptions), - ...constant.options, - }); - - const detailedOptions = arrayify(detailedOptionMap, "name"); - - const apiDefaultOptions = { - ...optionsModule.hiddenDefaults, - ...fromPairs( - supportOptions - .filter(({ deprecated }) => !deprecated) - .map((option) => [option.name, option.default]) - ), - }; - - Object.assign(this, { - supportOptions, - detailedOptions, - detailedOptionMap, - apiDefaultOptions, - languages, - }); - } } -module.exports = Context; +function getContextOptions(plugins, pluginSearchDirs) { + const { options: supportOptions, languages } = prettier.getSupportInfo({ + showDeprecated: true, + showUnreleased: true, + showInternal: true, + plugins, + pluginSearchDirs, + }); + const detailedOptionMap = normalizeDetailedOptionMap({ + ...createDetailedOptionMap(supportOptions), + ...constant.options, + }); + + const detailedOptions = arrayify(detailedOptionMap, "name"); + + const apiDefaultOptions = { + ...optionsModule.hiddenDefaults, + ...fromPairs( + supportOptions + .filter(({ deprecated }) => !deprecated) + .map((option) => [option.name, option.default]) + ), + }; + + return { + supportOptions, + detailedOptions, + detailedOptionMap, + apiDefaultOptions, + languages, + }; +} + +function parseArgv(rawArguments, detailedOptions, logger, keys) { + const minimistOptions = createMinimistOptions(detailedOptions); + let argv = minimist(rawArguments, minimistOptions); + + if (keys) { + detailedOptions = detailedOptions.filter((option) => + keys.includes(option.name) + ); + argv = pick(argv, keys); + } + + return normalizeCliOptions(argv, detailedOptions, { logger }); +} + +const detailedOptionsWithoutPlugins = getContextOptions().detailedOptions; +function parseArgvWithoutPlugins(rawArguments, logger, keys) { + return parseArgv( + rawArguments, + detailedOptionsWithoutPlugins, + logger, + typeof keys === "string" ? [keys] : keys + ); +} + +module.exports = { Context, parseArgvWithoutPlugins }; diff --git a/src/cli/core.js b/src/cli/core.js index d13397856..0beffc60c 100644 --- a/src/cli/core.js +++ b/src/cli/core.js @@ -8,21 +8,21 @@ const stringify = require("fast-json-stable-stringify"); const prettier = require("../index"); const { format, formatStdin, formatFiles } = require("./format"); -const Context = require("./context"); +const { Context, parseArgvWithoutPlugins } = require("./context"); const { normalizeDetailedOptionMap, createDetailedOptionMap, } = require("./option-map"); const { createDetailedUsage, createUsage } = require("./usage"); +const { createLogger } = require("./logger"); function logResolvedConfigPathOrDie(context) { - const configFile = prettier.resolveConfigFile.sync( - context.argv["find-config-path"] - ); + const file = context.argv["find-config-path"]; + const configFile = prettier.resolveConfigFile.sync(file); if (configFile) { context.logger.log(path.relative(process.cwd(), configFile)); } else { - process.exit(1); + throw new Error(`Can not find configure file for "${file}"`); } } @@ -54,4 +54,6 @@ module.exports = { logResolvedConfigPathOrDie, logFileInfoOrDie, normalizeDetailedOptionMap, + parseArgvWithoutPlugins, + createLogger, }; diff --git a/src/cli/expand-patterns.js b/src/cli/expand-patterns.js index 7dba07ddb..b8f3bbc55 100644 --- a/src/cli/expand-patterns.js +++ b/src/cli/expand-patterns.js @@ -33,7 +33,7 @@ function* expandPatterns(context) { yield relativePath; } - if (noResults) { + if (noResults && context.argv["error-on-unmatched-pattern"] !== false) { // If there was no files and no other errors, let's yield a general error. yield { error: `No matching files. Patterns: ${context.filePatterns.join(" ")}`, @@ -46,18 +46,13 @@ function* expandPatterns(context) { */ function* expandPatternsInternal(context) { // Ignores files in version control systems directories and `node_modules` - const silentlyIgnoredDirs = { - ".git": true, - ".svn": true, - ".hg": true, - node_modules: context.argv["with-node-modules"] !== true, - }; - + const silentlyIgnoredDirs = [".git", ".svn", ".hg"]; + if (context.argv["with-node-modules"] !== true) { + silentlyIgnoredDirs.push("node_modules"); + } const globOptions = { dot: true, - ignore: Object.keys(silentlyIgnoredDirs) - .filter((dir) => silentlyIgnoredDirs[dir]) - .map((dir) => "**/" + dir), + ignore: silentlyIgnoredDirs.map((dir) => "**/" + dir), }; let supportedFilesGlob; @@ -116,7 +111,9 @@ function* expandPatternsInternal(context) { } if (result.length === 0) { - yield { error: `${errorMessages.emptyResults[type]}: "${input}".` }; + if (context.argv["error-on-unmatched-pattern"] !== false) { + yield { error: `${errorMessages.emptyResults[type]}: "${input}".` }; + } } else { yield* sortPaths(result); } @@ -130,9 +127,10 @@ function* expandPatternsInternal(context) { const filenames = flat( context.languages.map((lang) => lang.filenames || []) ); - supportedFilesGlob = `**/{${extensions - .map((ext) => "*" + (ext[0] === "." ? ext : "." + ext)) - .concat(filenames)}}`; + supportedFilesGlob = `**/{${[ + ...extensions.map((ext) => "*" + (ext[0] === "." ? ext : "." + ext)), + ...filenames, + ]}}`; } return supportedFilesGlob; } @@ -154,13 +152,13 @@ const errorMessages = { /** * @param {string} absolutePath * @param {string} cwd - * @param {Record} ignoredDirectories + * @param {string[]} ignoredDirectories */ function containsIgnoredPathSegment(absolutePath, cwd, ignoredDirectories) { return path .relative(cwd, absolutePath) .split(path.sep) - .some((dir) => ignoredDirectories[dir]); + .some((dir) => ignoredDirectories.includes(dir)); } /** diff --git a/src/cli/format.js b/src/cli/format.js index 99e6f59d6..e9aef8ebb 100644 --- a/src/cli/format.js +++ b/src/cli/format.js @@ -1,7 +1,6 @@ "use strict"; const fs = require("fs"); -const readline = require("readline"); const path = require("path"); const chalk = require("chalk"); @@ -22,13 +21,15 @@ function diff(a, b) { }); } -function handleError(context, filename, error) { +function handleError(context, filename, error, printedFilename) { if (error instanceof errors.UndefinedParserError) { // Can't test on CI, `isTTY()` is always false, see ./is-tty.js /* istanbul ignore next */ - if ((context.argv.write || context.argv["ignore-unknown"]) && isTTY()) { - readline.clearLine(process.stdout, 0); - readline.cursorTo(process.stdout, 0, null); + if ( + (context.argv.write || context.argv["ignore-unknown"]) && + printedFilename + ) { + printedFilename.clear(); } if (context.argv["ignore-unknown"]) { return; @@ -113,7 +114,16 @@ function format(context, input, opt) { if (context.argv["debug-print-doc"]) { const doc = prettier.__debug.printToDoc(input, opt); - return { formatted: prettier.__debug.formatDoc(doc) }; + return { formatted: prettier.__debug.formatDoc(doc) + "\n" }; + } + + if (context.argv["debug-print-comments"]) { + return { + formatted: prettier.format( + JSON.stringify(prettier.formatWithCursor(input, opt).comments || []), + { parser: "json" } + ), + }; } if (context.argv["debug-check"]) { @@ -155,7 +165,7 @@ function format(context, input, opt) { let benchmark; try { benchmark = eval("require")("benchmark"); - } catch (err) { + } catch { context.logger.debug( "'--debug-benchmark' requires the 'benchmark' package to be installed." ); @@ -188,13 +198,12 @@ function format(context, input, opt) { repeat + " times." ); - // should be using `performance.now()`, but only `Date` is cross-platform enough - const now = Date.now ? () => Date.now() : () => +new Date(); let totalMs = 0; for (let i = 0; i < repeat; ++i) { - const startMs = now(); + // should be using `performance.now()`, but only `Date` is cross-platform enough + const startMs = Date.now(); prettier.formatWithCursor(input, opt); - totalMs += now() - startMs; + totalMs += Date.now() - startMs; } const averageMs = totalMs / repeat; const results = { @@ -300,8 +309,12 @@ function formatFiles(context) { filepath: filename, }; + let printedFilename; if (isTTY()) { - context.logger.log(filename, { newline: false }); + printedFilename = context.logger.log(filename, { + newline: false, + clearable: true, + }); } let input; @@ -339,16 +352,15 @@ function formatFiles(context) { result = format(context, input, options); output = result.formatted; } catch (error) { - handleError(context, filename, error); + handleError(context, filename, error, printedFilename); continue; } const isDifferent = output !== input; - if (isTTY()) { + if (printedFilename) { // Remove previously printed filename to log it with duration. - readline.clearLine(process.stdout, 0); - readline.cursorTo(process.stdout, 0, null); + printedFilename.clear(); } if (context.argv.write) { diff --git a/src/cli/index.js b/src/cli/index.js index 0ef3099ca..b38f6f511 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -8,77 +8,87 @@ const stringify = require("fast-json-stable-stringify"); const prettier = require("../index"); const core = require("./core"); -function run(args) { - const context = new core.Context(args); +function run(rawArguments) { + // Create a default level logger, so we can log errors during `logLevel` parsing + let logger = core.createLogger(); try { - context.initContext(); - - context.logger.debug(`normalized argv: ${JSON.stringify(context.argv)}`); - - if (context.argv.check && context.argv["list-different"]) { - context.logger.error("Cannot use --check and --list-different together."); - process.exit(1); + const logLevel = core.parseArgvWithoutPlugins( + rawArguments, + logger, + "loglevel" + ).loglevel; + if (logLevel !== logger.logLevel) { + logger = core.createLogger(logLevel); } - if (context.argv.write && context.argv["debug-check"]) { - context.logger.error("Cannot use --write and --debug-check together."); - process.exit(1); - } - - if (context.argv["find-config-path"] && context.filePatterns.length > 0) { - context.logger.error("Cannot use --find-config-path with multiple files"); - process.exit(1); - } - - if (context.argv["file-info"] && context.filePatterns.length > 0) { - context.logger.error("Cannot use --file-info with multiple files"); - process.exit(1); - } - - if (context.argv.version) { - context.logger.log(prettier.version); - process.exit(0); - } - - if (context.argv.help !== undefined) { - context.logger.log( - typeof context.argv.help === "string" && context.argv.help !== "" - ? core.createDetailedUsage(context, context.argv.help) - : core.createUsage(context) - ); - process.exit(0); - } - - if (context.argv["support-info"]) { - context.logger.log( - prettier.format(stringify(prettier.getSupportInfo()), { - parser: "json", - }) - ); - process.exit(0); - } - - const hasFilePatterns = context.filePatterns.length > 0; - const useStdin = - !hasFilePatterns && - (!process.stdin.isTTY || context.args["stdin-filepath"]); - - if (context.argv["find-config-path"]) { - core.logResolvedConfigPathOrDie(context); - } else if (context.argv["file-info"]) { - core.logFileInfoOrDie(context); - } else if (useStdin) { - core.formatStdin(context); - } else if (hasFilePatterns) { - core.formatFiles(context); - } else { - context.logger.log(core.createUsage(context)); - process.exit(1); - } + main(rawArguments, logger); } catch (error) { - context.logger.error(error.message); - process.exit(1); + logger.error(error.message); + process.exitCode = 1; + } +} + +function main(rawArguments, logger) { + const context = new core.Context({ rawArguments, logger }); + + logger.debug(`normalized argv: ${JSON.stringify(context.argv)}`); + + if (context.argv.check && context.argv["list-different"]) { + throw new Error("Cannot use --check and --list-different together."); + } + + if (context.argv.write && context.argv["debug-check"]) { + throw new Error("Cannot use --write and --debug-check together."); + } + + if (context.argv["find-config-path"] && context.filePatterns.length > 0) { + throw new Error("Cannot use --find-config-path with multiple files"); + } + + if (context.argv["file-info"] && context.filePatterns.length > 0) { + throw new Error("Cannot use --file-info with multiple files"); + } + + if (context.argv.version) { + logger.log(prettier.version); + return; + } + + if (context.argv.help !== undefined) { + logger.log( + typeof context.argv.help === "string" && context.argv.help !== "" + ? core.createDetailedUsage(context, context.argv.help) + : core.createUsage(context) + ); + return; + } + + if (context.argv["support-info"]) { + logger.log( + prettier.format(stringify(prettier.getSupportInfo()), { + parser: "json", + }) + ); + return; + } + + const hasFilePatterns = context.filePatterns.length > 0; + const useStdin = + !hasFilePatterns && + (!process.stdin.isTTY || context.argv["stdin-filepath"]); + + if (context.argv["find-config-path"]) { + core.logResolvedConfigPathOrDie(context); + } else if (context.argv["file-info"]) { + core.logFileInfoOrDie(context); + } else if (useStdin) { + core.formatStdin(context); + } else if (hasFilePatterns) { + core.formatFiles(context); + } else { + logger.log(core.createUsage(context)); + process.exitCode = 1; } } diff --git a/src/cli/logger.js b/src/cli/logger.js index c899381d5..88c97e820 100644 --- a/src/cli/logger.js +++ b/src/cli/logger.js @@ -1,9 +1,36 @@ "use strict"; +const readline = require("readline"); const chalk = require("chalk"); +const stripAnsi = require("strip-ansi"); +const wcwidth = require("wcwidth"); -function createLogger(logLevel) { +const countLines = (stream, text) => { + const columns = stream.columns || 80; + let lineCount = 0; + for (const line of stripAnsi(text).split("\n")) { + lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns)); + } + return lineCount; +}; + +const clear = (stream, text) => () => { + const lineCount = countLines(stream, text); + + for (let line = 0; line < lineCount; line++) { + if (line > 0) { + readline.moveCursor(stream, 0, -1); + } + + readline.clearLine(stream, 0); + readline.cursorTo(stream, 0); + } +}; + +const emptyLogResult = { clear() {} }; +function createLogger(logLevel = "log") { return { + logLevel, warn: createLogFunc("warn", "yellow"), error: createLogFunc("error", "red"), debug: createLogFunc("debug", "blue"), @@ -12,14 +39,26 @@ function createLogger(logLevel) { function createLogFunc(loggerName, color) { if (!shouldLog(loggerName)) { - return () => {}; + return () => emptyLogResult; } const prefix = color ? `[${chalk[color](loggerName)}] ` : ""; - return function (message, opts) { - opts = { newline: true, ...opts }; - const stream = process[loggerName === "log" ? "stdout" : "stderr"]; - stream.write(message.replace(/^/gm, prefix) + (opts.newline ? "\n" : "")); + const stream = process[loggerName === "log" ? "stdout" : "stderr"]; + + return (message, options) => { + options = { + newline: true, + clearable: false, + ...options, + }; + message = message.replace(/^/gm, prefix) + (options.newline ? "\n" : ""); + stream.write(message); + + if (options.clearable) { + return { + clear: clear(stream, message), + }; + } }; } diff --git a/src/cli/option.js b/src/cli/option.js index b71726382..bb5db7aa5 100644 --- a/src/cli/option.js +++ b/src/cli/option.js @@ -20,13 +20,14 @@ function getOptions(argv, detailedOptions) { } function cliifyOptions(object, apiDetailedOptionMap) { - return Object.keys(object || {}).reduce((output, key) => { - const apiOption = apiDetailedOptionMap[key]; - const cliKey = apiOption ? apiOption.name : key; + return fromPairs( + Object.entries(object || {}).map(([key, value]) => { + const apiOption = apiDetailedOptionMap[key]; + const cliKey = apiOption ? apiOption.name : key; - output[dashify(cliKey)] = object[key]; - return output; - }, {}); + return [dashify(cliKey), value]; + }) + ); } function createApiDetailedOptionMap(detailedOptions) { @@ -46,7 +47,7 @@ function parseArgsToOptions(context, overrideDefaults) { ); return getOptions( optionsNormalizer.normalizeCliOptions( - minimist(context.args, { + minimist(context.rawArguments, { string: minimistOptions.string, boolean: minimistOptions.boolean, default: cliifyOptions(overrideDefaults, apiDetailedOptionMap), diff --git a/src/cli/usage.js b/src/cli/usage.js index 2babc28e3..2685b3bc6 100644 --- a/src/cli/usage.js +++ b/src/cli/usage.js @@ -148,7 +148,7 @@ function createUsage(context) { return `${category} options:\n\n${indent(categoryOptions, 2)}`; }); - return [constant.usageSummary].concat(optionsUsage, [""]).join("\n\n"); + return [constant.usageSummary, ...optionsUsage, ""].join("\n\n"); } function createDetailedUsage(context, flag) { @@ -176,11 +176,8 @@ function createDetailedUsage(context, flag) { const pluginDefaults = option.pluginDefaults && Object.keys(option.pluginDefaults).length > 0 - ? `\nPlugin defaults:${Object.keys(option.pluginDefaults).map( - (key) => - `\n* ${key}: ${createDefaultValueDisplay( - option.pluginDefaults[key] - )}` + ? `\nPlugin defaults:${Object.entries(option.pluginDefaults).map( + ([key, value]) => `\n* ${key}: ${createDefaultValueDisplay(value)}` )}` : ""; return `${header}${description}${choices}${defaults}${pluginDefaults}`; diff --git a/src/common/fast-path.js b/src/common/ast-path.js similarity index 75% rename from src/common/fast-path.js rename to src/common/ast-path.js index 02ad5893a..0dd5a3826 100644 --- a/src/common/fast-path.js +++ b/src/common/ast-path.js @@ -16,7 +16,7 @@ function getNodeStackIndexHelper(stack, count) { return -1; } -class FastPath { +class AstPath { constructor(value) { this.stack = [value]; } @@ -51,7 +51,7 @@ class FastPath { // Temporarily push properties named by string arguments given after the // callback function onto this.stack, then call the callback with a - // reference to this (modified) FastPath object. Note that the stack will + // reference to this (modified) AstPath object. Note that the stack will // be restored to its original state after the callback is finished, so it // is probably a mistake to retain a reference to the path. call(callback, ...names) { @@ -76,7 +76,7 @@ class FastPath { return result; } - // Similar to FastPath.prototype.call, except that the value obtained by + // Similar to AstPath.prototype.call, except that the value obtained by // accessing this.getValue()[name1][name2]... should be array. The // callback will be called with a reference to this path object for each // element of the array. @@ -99,7 +99,7 @@ class FastPath { stack.length = length; } - // Similar to FastPath.prototype.each, except that the results of the + // Similar to AstPath.prototype.each, except that the results of the // callback function invocations are stored in an array and returned at // the end of the iteration. map(callback, ...names) { @@ -146,6 +146,37 @@ class FastPath { return true; } + + /** + * Traverses the ancestors of the current node heading toward the tree root + * until it finds a node that matches the provided predicate function. Will + * return the first matching ancestor. If no such node exists, returns undefined. + * @param {(node: any, name: string, number: number | null) => boolean} predicate + * @internal Unstable API. Don't use in plugins for now. + */ + findAncestor(predicate) { + let stackPointer = this.stack.length - 1; + + let name = null; + let node = this.stack[stackPointer--]; + + while (node) { + // skip index/array + let number = null; + if (typeof name === "number") { + number = name; + name = this.stack[stackPointer--]; + node = this.stack[stackPointer--]; + } + + if (name !== null && predicate(node, name, number)) { + return node; + } + + name = this.stack[stackPointer--]; + node = this.stack[stackPointer--]; + } + } } -module.exports = FastPath; +module.exports = AstPath; diff --git a/src/common/load-plugins.js b/src/common/load-plugins.js index 6b1934133..6f0f3a527 100644 --- a/src/common/load-plugins.js +++ b/src/common/load-plugins.js @@ -4,6 +4,7 @@ const fs = require("fs"); const path = require("path"); const uniqBy = require("lodash/uniqBy"); const partition = require("lodash/partition"); +const flatten = require("lodash/flatten"); const globby = require("globby"); const mem = require("mem"); const internalPlugins = require("../languages"); @@ -44,7 +45,7 @@ function load(plugins, pluginSearchDirs) { try { // try local files requirePath = resolve(path.resolve(process.cwd(), pluginName)); - } catch (_) { + } catch { // try node modules requirePath = resolve(pluginName, { paths: [process.cwd()] }); } @@ -56,8 +57,8 @@ function load(plugins, pluginSearchDirs) { } ); - const externalAutoLoadPluginInfos = pluginSearchDirs - .map((pluginSearchDir) => { + const externalAutoLoadPluginInfos = flatten( + pluginSearchDirs.map((pluginSearchDir) => { const resolvedPluginSearchDir = path.resolve( process.cwd(), pluginSearchDir @@ -85,19 +86,20 @@ function load(plugins, pluginSearchDirs) { requirePath: resolve(pluginName, { paths: [resolvedPluginSearchDir] }), })); }) - .reduce((a, b) => a.concat(b), []); + ); - const externalPlugins = uniqBy( - externalManualLoadPluginInfos.concat(externalAutoLoadPluginInfos), - "requirePath" - ) - .map((externalPluginInfo) => ({ + const externalPlugins = [ + ...uniqBy( + [...externalManualLoadPluginInfos, ...externalAutoLoadPluginInfos], + "requirePath" + ).map((externalPluginInfo) => ({ name: externalPluginInfo.name, ...eval("require")(externalPluginInfo.requirePath), - })) - .concat(externalPluginInstances); + })), + ...externalPluginInstances, + ]; - return internalPlugins.concat(externalPlugins); + return [...internalPlugins, ...externalPlugins]; } function findPluginsInNodeModules(nodeModulesDir) { @@ -118,7 +120,7 @@ function findPluginsInNodeModules(nodeModulesDir) { function isDirectory(dir) { try { return fs.statSync(dir).isDirectory(); - } catch (e) { + } catch { return false; } } diff --git a/src/common/util.js b/src/common/util.js index ca986ee3b..41bf49780 100644 --- a/src/common/util.js +++ b/src/common/util.js @@ -164,8 +164,7 @@ function skipNewline(text, index, opts) { * @param {SkipOptions=} opts * @returns {boolean} */ -function hasNewline(text, index, opts) { - opts = opts || {}; +function hasNewline(text, index, opts = {}) { const idx = skipSpaces(text, opts.backwards ? index - 1 : index, opts); const idx2 = skipNewline(text, idx, opts); return idx !== idx2; @@ -292,8 +291,7 @@ function getNextNonSpaceNonCommentCharacter(text, node, locEnd) { * @param {SkipOptions=} opts * @returns {boolean} */ -function hasSpaces(text, index, opts) { - opts = opts || {}; +function hasSpaces(text, index, opts = {}) { const idx = skipSpaces(text, opts.backwards ? index - 1 : index, opts); return idx !== index; } @@ -304,9 +302,7 @@ function hasSpaces(text, index, opts) { * @param {number=} startIndex * @returns {number} */ -function getAlignmentSize(value, tabWidth, startIndex) { - startIndex = startIndex || 0; - +function getAlignmentSize(value, tabWidth, startIndex = 0) { let size = 0; for (let i = startIndex; i < value.length; ++i) { if (value[i] === "\t") { @@ -385,35 +381,22 @@ function getPreferredQuote(raw, preferredQuote) { return result; } -function printString(raw, options, isDirectiveLiteral) { +function printString(raw, options) { // `rawContent` is the string exactly like it appeared in the input source // code, without its enclosing quotes. const rawContent = raw.slice(1, -1); - // Check for the alternate quote, to determine if we're allowed to swap - // the quotes on a DirectiveLiteral. - const canChangeDirectiveQuotes = - !rawContent.includes('"') && !rawContent.includes("'"); - /** @type {Quote} */ const enclosingQuote = - options.parser === "json" + options.parser === "json" || + (options.parser === "json5" && + options.quoteProps === "preserve" && + !options.singleQuote) ? '"' : options.__isInHtmlAttribute ? "'" : getPreferredQuote(raw, options.singleQuote ? "'" : '"'); - // Directives are exact code unit sequences, which means that you can't - // change the escape sequences they use. - // See https://github.com/prettier/prettier/issues/1555 - // and https://tc39.github.io/ecma262/#directive-prologue - if (isDirectiveLiteral) { - if (canChangeDirectiveQuotes) { - return enclosingQuote + rawContent + enclosingQuote; - } - return raw; - } - // It might sound unnecessary to use `makeString` even if the string already // is enclosed with `enclosingQuote`, but it isn't. The string could contain // unnecessary escapes (such as in `"\'"`). Always using `makeString` makes @@ -425,7 +408,7 @@ function printString(raw, options, isDirectiveLiteral) { options.parser === "css" || options.parser === "less" || options.parser === "scss" || - options.embeddedInHtml + options.__embeddedInHtml ) ); } @@ -561,6 +544,7 @@ function addCommentHelper(node, comment) { const comments = node.comments || (node.comments = []); comments.push(comment); comment.printed = false; + comment.nodeDescription = describeNodeForDebugging(node); } function addLeadingComment(node, comment) { @@ -642,6 +626,23 @@ function createGroupIdMapper(description) { }; } +function describeNodeForDebugging(node) { + const nodeType = node.type || node.kind || "(unknown type)"; + let nodeName = String( + node.name || + (node.id && (typeof node.id === "object" ? node.id.name : node.id)) || + (node.key && (typeof node.key === "object" ? node.key.name : node.key)) || + (node.value && + (typeof node.value === "object" ? "" : String(node.value))) || + node.operator || + "" + ); + if (nodeName.length > 20) { + nodeName = nodeName.slice(0, 19) + "…"; + } + return nodeType + (nodeName ? " " + nodeName : ""); +} + module.exports = { inferParserByLanguage, replaceEndOfLineWith, diff --git a/src/config/resolve-config.js b/src/config/resolve-config.js index a8c55977a..441e6ff9c 100644 --- a/src/config/resolve-config.js +++ b/src/config/resolve-config.js @@ -68,9 +68,9 @@ function getExplorer(opts) { function _resolveConfig(filePath, opts, sync) { opts = { useCache: true, ...opts }; const loadOpts = { - cache: !!opts.useCache, - sync: !!sync, - editorconfig: !!opts.editorconfig, + cache: Boolean(opts.useCache), + sync: Boolean(sync), + editorconfig: Boolean(opts.editorconfig), }; const { load, search } = getExplorer(loadOpts); const loadEditorConfig = resolveEditorConfig.getLoadFunction(loadOpts); @@ -154,9 +154,11 @@ function mergeOverrides(configResult, filePath) { } // Based on eslint: https://github.com/eslint/eslint/blob/master/lib/config/config-ops.js -function pathMatchesGlobs(filePath, patterns, excludedPatterns) { - const patternList = [].concat(patterns); - const excludedPatternList = [].concat(excludedPatterns || []); +function pathMatchesGlobs(filePath, patterns, excludedPatterns = []) { + const patternList = Array.isArray(patterns) ? patterns : [patterns]; + const excludedPatternList = Array.isArray(excludedPatterns) + ? excludedPatterns + : [excludedPatterns]; const opts = { matchBase: true, dot: true }; return ( diff --git a/src/document/doc-builders.js b/src/document/doc-builders.js index a875f9be0..4b0315070 100644 --- a/src/document/doc-builders.js +++ b/src/document/doc-builders.js @@ -86,9 +86,7 @@ function align(n, contents) { * @param {object} [opts] - TBD ??? * @returns Doc */ -function group(contents, opts) { - opts = opts || {}; - +function group(contents, opts = {}) { if (process.env.NODE_ENV !== "production") { assertDoc(contents); } @@ -97,7 +95,7 @@ function group(contents, opts) { type: "group", id: opts.id, contents, - break: !!opts.shouldBreak, + break: Boolean(opts.shouldBreak), expandedStates: opts.expandedStates, }; } @@ -156,9 +154,7 @@ function fill(parts) { * @param {object} [opts] - TBD ??? * @returns Doc */ -function ifBreak(breakContents, flatContents, opts) { - opts = opts || {}; - +function ifBreak(breakContents, flatContents, opts = {}) { if (process.env.NODE_ENV !== "production") { if (breakContents) { assertDoc(breakContents); @@ -176,6 +172,21 @@ function ifBreak(breakContents, flatContents, opts) { }; } +/** + * Optimized version of `ifBreak(indent(doc), doc, { groupId: ... })` + * @param {Doc} contents + * @param {{ groupId: symbol, negate?: boolean }} opts + * @returns Doc + */ +function indentIfBreak(contents, opts) { + return { + type: "indent-if-break", + contents, + groupId: opts.groupId, + negate: opts.negate, + }; +} + /** * @param {Doc} contents * @returns Doc @@ -190,13 +201,21 @@ function lineSuffix(contents) { const lineSuffixBoundary = { type: "line-suffix-boundary" }; const breakParent = { type: "break-parent" }; const trim = { type: "trim" }; + +const hardlineWithoutBreakParent = { type: "line", hard: true }; +const literallineWithoutBreakParent = { + type: "line", + hard: true, + literal: true, +}; + const line = { type: "line" }; const softline = { type: "line", soft: true }; -const hardline = concat([{ type: "line", hard: true }, breakParent]); -const literalline = concat([ - { type: "line", hard: true, literal: true }, - breakParent, -]); +// eslint-disable-next-line prettier-internal-rules/no-doc-builder-concat +const hardline = concat([hardlineWithoutBreakParent, breakParent]); +// eslint-disable-next-line prettier-internal-rules/no-doc-builder-concat +const literalline = concat([literallineWithoutBreakParent, breakParent]); + const cursor = { type: "cursor", placeholder: Symbol("cursor") }; /** @@ -215,6 +234,7 @@ function join(sep, arr) { res.push(arr[i]); } + // eslint-disable-next-line prettier-internal-rules/no-doc-builder-concat return concat(res); } @@ -239,6 +259,10 @@ function addAlignmentToDoc(doc, size, tabWidth) { return aligned; } +function label(label, contents) { + return { type: "label", label, contents }; +} + module.exports = { concat, join, @@ -256,9 +280,13 @@ module.exports = { ifBreak, trim, indent, + indentIfBreak, align, addAlignmentToDoc, markAsRoot, dedentToRoot, dedent, + hardlineWithoutBreakParent, + literallineWithoutBreakParent, + label, }; diff --git a/src/document/doc-debug.js b/src/document/doc-debug.js index 0c31aa99f..6d7050929 100644 --- a/src/document/doc-debug.js +++ b/src/document/doc-debug.js @@ -3,15 +3,17 @@ const { isConcat, getDocParts } = require("./doc-utils"); function flattenDoc(doc) { + if (!doc) { + return ""; + } + if (isConcat(doc)) { const res = []; - const parts = getDocParts(doc); - for (let i = 0; i < parts.length; ++i) { - const doc2 = parts[i]; - if (typeof doc2 !== "string" && isConcat(doc2)) { - res.push(...flattenDoc(doc2).parts); + for (const part of getDocParts(doc)) { + if (isConcat(part)) { + res.push(...flattenDoc(part).parts); } else { - const flattened = flattenDoc(doc2); + const flattened = flattenDoc(part); if (flattened !== "") { res.push(flattened); } @@ -19,116 +21,193 @@ function flattenDoc(doc) { } return { type: "concat", parts: res }; - } else if (doc.type === "if-break") { - return { - ...doc, - breakContents: - doc.breakContents != null ? flattenDoc(doc.breakContents) : null, - flatContents: - doc.flatContents != null ? flattenDoc(doc.flatContents) : null, - }; - } else if (doc.type === "group") { - return { - ...doc, - contents: flattenDoc(doc.contents), - expandedStates: doc.expandedStates - ? doc.expandedStates.map(flattenDoc) - : doc.expandedStates, - }; - } else if (doc.contents) { - return { ...doc, contents: flattenDoc(doc.contents) }; - } - return doc; -} - -function printDoc(doc) { - if (typeof doc === "string") { - return JSON.stringify(doc); - } - - if (isConcat(doc)) { - return "[" + getDocParts(doc).map(printDoc).join(", ") + "]"; - } - - if (doc.type === "line") { - if (doc.literal) { - return "literalline"; - } - if (doc.hard) { - return "hardline"; - } - if (doc.soft) { - return "softline"; - } - return "line"; - } - - if (doc.type === "break-parent") { - return "breakParent"; - } - - if (doc.type === "trim") { - return "trim"; - } - - if (doc.type === "indent") { - return "indent(" + printDoc(doc.contents) + ")"; - } - - if (doc.type === "align") { - return doc.n === Number.NEGATIVE_INFINITY - ? "dedentToRoot(" + printDoc(doc.contents) + ")" - : doc.n < 0 - ? "dedent(" + printDoc(doc.contents) + ")" - : doc.n.type === "root" - ? "markAsRoot(" + printDoc(doc.contents) + ")" - : "align(" + JSON.stringify(doc.n) + ", " + printDoc(doc.contents) + ")"; } if (doc.type === "if-break") { - return ( - "ifBreak(" + - printDoc(doc.breakContents) + - (doc.flatContents ? ", " + printDoc(doc.flatContents) : "") + - ")" - ); + return { + ...doc, + breakContents: flattenDoc(doc.breakContents), + flatContents: flattenDoc(doc.flatContents), + }; } if (doc.type === "group") { - if (doc.expandedStates) { - return ( - "conditionalGroup(" + - "[" + - doc.expandedStates.map(printDoc).join(",") + - "])" - ); - } - - return ( - (doc.break ? "wrappedGroup" : "group") + - "(" + - printDoc(doc.contents) + - ")" - ); + return { + ...doc, + contents: flattenDoc(doc.contents), + expandedStates: doc.expandedStates && doc.expandedStates.map(flattenDoc), + }; } if (doc.type === "fill") { - return "fill" + "(" + doc.parts.map(printDoc).join(", ") + ")"; + return { type: "fill", parts: doc.parts.map(flattenDoc) }; } - if (doc.type === "line-suffix") { - return "lineSuffix(" + printDoc(doc.contents) + ")"; + if (doc.contents) { + return { ...doc, contents: flattenDoc(doc.contents) }; } - if (doc.type === "line-suffix-boundary") { - return "lineSuffixBoundary"; - } - - throw new Error("Unknown doc type " + doc.type); + return doc; } -module.exports = { - printDocToDebug(doc) { - return printDoc(flattenDoc(doc)); - }, -}; +function printDocToDebug(doc) { + /** @type Record */ + const printedSymbols = Object.create(null); + /** @type Set */ + const usedKeysForSymbols = new Set(); + return printDoc(flattenDoc(doc)); + + function printDoc(doc, index, parentParts) { + if (typeof doc === "string") { + return JSON.stringify(doc); + } + + if (isConcat(doc)) { + const printed = getDocParts(doc).map(printDoc).filter(Boolean); + return printed.length === 1 ? printed[0] : `[${printed.join(", ")}]`; + } + + if (doc.type === "line") { + const withBreakParent = + Array.isArray(parentParts) && + parentParts[index + 1] && + parentParts[index + 1].type === "break-parent"; + if (doc.literal) { + return withBreakParent + ? "literalline" + : "literallineWithoutBreakParent"; + } + if (doc.hard) { + return withBreakParent ? "hardline" : "hardlineWithoutBreakParent"; + } + if (doc.soft) { + return "softline"; + } + return "line"; + } + + if (doc.type === "break-parent") { + const afterHardline = + Array.isArray(parentParts) && + parentParts[index - 1] && + parentParts[index - 1].type === "line" && + parentParts[index - 1].hard; + return afterHardline ? undefined : "breakParent"; + } + + if (doc.type === "trim") { + return "trim"; + } + + if (doc.type === "indent") { + return "indent(" + printDoc(doc.contents) + ")"; + } + + if (doc.type === "align") { + return doc.n === Number.NEGATIVE_INFINITY + ? "dedentToRoot(" + printDoc(doc.contents) + ")" + : doc.n < 0 + ? "dedent(" + printDoc(doc.contents) + ")" + : doc.n.type === "root" + ? "markAsRoot(" + printDoc(doc.contents) + ")" + : "align(" + + JSON.stringify(doc.n) + + ", " + + printDoc(doc.contents) + + ")"; + } + + if (doc.type === "if-break") { + return ( + "ifBreak(" + + printDoc(doc.breakContents) + + (doc.flatContents ? ", " + printDoc(doc.flatContents) : "") + + (doc.groupId + ? (!doc.flatContents ? ', ""' : "") + + `, { groupId: ${printGroupId(doc.groupId)} }` + : "") + + ")" + ); + } + + if (doc.type === "indent-if-break") { + const optionsParts = []; + + if (doc.negate) { + optionsParts.push("negate: true"); + } + + if (doc.groupId) { + optionsParts.push(`groupId: ${printGroupId(doc.groupId)}`); + } + + const options = + optionsParts.length > 0 ? `, { ${optionsParts.join(", ")} }` : ""; + + return `indentIfBreak(${printDoc(doc.contents)}${options})`; + } + + if (doc.type === "group") { + const optionsParts = []; + + if (doc.break && doc.break !== "propagated") { + optionsParts.push("shouldBreak: true"); + } + + if (doc.id) { + optionsParts.push(`id: ${printGroupId(doc.id)}`); + } + + const options = + optionsParts.length > 0 ? `, { ${optionsParts.join(", ")} }` : ""; + + if (doc.expandedStates) { + return `conditionalGroup([${doc.expandedStates + .map((part) => printDoc(part)) + .join(",")}]${options})`; + } + + return `group(${printDoc(doc.contents)}${options})`; + } + + if (doc.type === "fill") { + return `fill([${doc.parts.map((part) => printDoc(part)).join(", ")}])`; + } + + if (doc.type === "line-suffix") { + return "lineSuffix(" + printDoc(doc.contents) + ")"; + } + + if (doc.type === "line-suffix-boundary") { + return "lineSuffixBoundary"; + } + + if (doc.type === "label") { + return `label(${JSON.stringify(doc.label)}, ${printDoc(doc.contents)})`; + } + + throw new Error("Unknown doc type " + doc.type); + } + + function printGroupId(id) { + if (typeof id !== "symbol") { + return JSON.stringify(String(id)); + } + + if (id in printedSymbols) { + return printedSymbols[id]; + } + + // TODO: use Symbol.prototype.description instead of slice once Node 10 is dropped + const prefix = String(id).slice(7, -1) || "symbol"; + for (let counter = 0; ; counter++) { + const key = prefix + (counter > 0 ? ` #${counter}` : ""); + if (!usedKeysForSymbols.has(key)) { + usedKeysForSymbols.add(key); + return (printedSymbols[id] = `Symbol.for(${JSON.stringify(key)})`); + } + } + } +} + +module.exports = { printDocToDebug }; diff --git a/src/document/doc-printer.js b/src/document/doc-printer.js index 897fb9b11..293f42bec 100644 --- a/src/document/doc-printer.js +++ b/src/document/doc-printer.js @@ -1,8 +1,8 @@ "use strict"; -const { getStringWidth } = require("../common/util"); +const { getStringWidth, getLast } = require("../common/util"); const { convertEndOfLineToChars } = require("../common/end-of-line"); -const { concat, fill, cursor } = require("./doc-builders"); +const { fill, cursor, indent } = require("./doc-builders"); const { isConcat, getDocParts } = require("./doc-utils"); /** @type {Record} */ @@ -45,7 +45,7 @@ function generateInd(ind, newPart, options) { const queue = newPart.type === "dedent" ? ind.queue.slice(0, -1) - : ind.queue.concat(newPart); + : [...ind.queue, newPart]; let value = ""; let length = 0; @@ -144,7 +144,7 @@ function trim(out) { return trimCount; } -function fits(next, restCommands, width, options, mustBeFlat) { +function fits(next, restCommands, width, options, hasLineSuffix, mustBeFlat) { let restIdx = restCommands.length; const cmds = [next]; // `out` is only used for width counting because `trim` requires to look @@ -187,32 +187,54 @@ function fits(next, restCommands, width, options, mustBeFlat) { width += trim(out); break; - case "group": + case "group": { if (mustBeFlat && doc.break) { return false; } - cmds.push([ind, doc.break ? MODE_BREAK : mode, doc.contents]); + const groupMode = doc.break ? MODE_BREAK : mode; + cmds.push([ + ind, + groupMode, + // The most expanded state takes up the least space on the current line. + doc.expandedStates && groupMode === MODE_BREAK + ? getLast(doc.expandedStates) + : doc.contents, + ]); if (doc.id) { - groupModeMap[doc.id] = cmds[cmds.length - 1][1]; + groupModeMap[doc.id] = groupMode; } break; + } case "fill": for (let i = doc.parts.length - 1; i >= 0; i--) { cmds.push([ind, mode, doc.parts[i]]); } break; - case "if-break": { + case "if-break": + case "indent-if-break": { const groupMode = doc.groupId ? groupModeMap[doc.groupId] : mode; if (groupMode === MODE_BREAK) { - if (doc.breakContents) { - cmds.push([ind, mode, doc.breakContents]); + const breakContents = + doc.type === "if-break" + ? doc.breakContents + : doc.negate + ? doc.contents + : indent(doc.contents); + if (breakContents) { + cmds.push([ind, mode, breakContents]); } } if (groupMode === MODE_FLAT) { - if (doc.flatContents) { - cmds.push([ind, mode, doc.flatContents]); + const flatContents = + doc.type === "if-break" + ? doc.flatContents + : doc.negate + ? indent(doc.contents) + : doc.contents; + if (flatContents) { + cmds.push([ind, mode, flatContents]); } } @@ -237,6 +259,17 @@ function fits(next, restCommands, width, options, mustBeFlat) { return true; } break; + case "line-suffix": + hasLineSuffix = true; + break; + case "line-suffix-boundary": + if (hasLineSuffix) { + return false; + } + break; + case "label": + cmds.push([ind, mode, doc.contents]); + break; } } } @@ -306,8 +339,9 @@ function printDocToString(doc, options) { const next = [ind, MODE_FLAT, doc.contents]; const rem = width - pos; + const hasLineSuffix = lineSuffix.length > 0; - if (!doc.break && fits(next, cmds, rem, options)) { + if (!doc.break && fits(next, cmds, rem, options, hasLineSuffix)) { cmds.push(next); } else { // Expanded states are a rare case where a document @@ -335,7 +369,7 @@ function printDocToString(doc, options) { const state = doc.expandedStates[i]; const cmd = [ind, MODE_FLAT, state]; - if (fits(cmd, cmds, rem, options)) { + if (fits(cmd, cmds, rem, options, hasLineSuffix)) { cmds.push(cmd); break; @@ -387,7 +421,14 @@ function printDocToString(doc, options) { const [content, whitespace] = parts; const contentFlatCmd = [ind, MODE_FLAT, content]; const contentBreakCmd = [ind, MODE_BREAK, content]; - const contentFits = fits(contentFlatCmd, [], rem, options, true); + const contentFits = fits( + contentFlatCmd, + [], + rem, + options, + lineSuffix.length > 0, + true + ); if (parts.length === 1) { if (contentFits) { @@ -423,13 +464,14 @@ function printDocToString(doc, options) { const firstAndSecondContentFlatCmd = [ ind, MODE_FLAT, - concat([content, whitespace, secondContent]), + [content, whitespace, secondContent], ]; const firstAndSecondContentFits = fits( firstAndSecondContentFlatCmd, [], rem, options, + lineSuffix.length > 0, true ); @@ -442,16 +484,29 @@ function printDocToString(doc, options) { } break; } - case "if-break": { + case "if-break": + case "indent-if-break": { const groupMode = doc.groupId ? groupModeMap[doc.groupId] : mode; if (groupMode === MODE_BREAK) { - if (doc.breakContents) { - cmds.push([ind, mode, doc.breakContents]); + const breakContents = + doc.type === "if-break" + ? doc.breakContents + : doc.negate + ? doc.contents + : indent(doc.contents); + if (breakContents) { + cmds.push([ind, mode, breakContents]); } } if (groupMode === MODE_FLAT) { - if (doc.flatContents) { - cmds.push([ind, mode, doc.flatContents]); + const flatContents = + doc.type === "if-break" + ? doc.flatContents + : doc.negate + ? indent(doc.contents) + : doc.contents; + if (flatContents) { + cmds.push([ind, mode, flatContents]); } } @@ -510,6 +565,9 @@ function printDocToString(doc, options) { break; } break; + case "label": + cmds.push([ind, mode, doc.contents]); + break; default: } } diff --git a/src/document/doc-utils.js b/src/document/doc-utils.js index 34f1ba1f2..649a1cfd5 100644 --- a/src/document/doc-utils.js +++ b/src/document/doc-utils.js @@ -1,9 +1,20 @@ "use strict"; -const { literalline, concat } = require("./doc-builders"); +const { literalline } = require("./doc-builders"); const isConcat = (doc) => Array.isArray(doc) || (doc && doc.type === "concat"); -const getDocParts = (doc) => (Array.isArray(doc) ? doc : doc.parts); +const getDocParts = (doc) => { + if (Array.isArray(doc)) { + return doc; + } + + /* istanbul ignore next */ + if (doc.type !== "concat" && doc.type !== "fill") { + throw new Error("Expect doc type to be `concat` or `fill`."); + } + + return doc.parts; +}; // Using a unique object to compare by reference. const traverseDocOnExitStackMarker = {}; @@ -62,14 +73,20 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) { function mapDoc(doc, cb) { if (Array.isArray(doc)) { return cb(doc.map((part) => mapDoc(part, cb))); - } else if (doc.type === "concat" || doc.type === "fill") { + } + + if (doc.type === "concat" || doc.type === "fill") { const parts = doc.parts.map((part) => mapDoc(part, cb)); return cb({ ...doc, parts }); - } else if (doc.type === "if-break") { + } + + if (doc.type === "if-break") { const breakContents = doc.breakContents && mapDoc(doc.breakContents, cb); const flatContents = doc.flatContents && mapDoc(doc.flatContents, cb); return cb({ ...doc, breakContents, flatContents }); - } else if (doc.contents) { + } + + if (doc.contents) { const contents = mapDoc(doc.contents, cb); return cb({ ...doc, contents }); } @@ -93,23 +110,6 @@ function findInDoc(doc, fn, defaultValue) { return result; } -function isEmpty(n) { - return typeof n === "string" && n.length === 0; -} - -function isLineNextFn(doc) { - if (typeof doc === "string") { - return false; - } - if (doc.type === "line") { - return true; - } -} - -function isLineNext(doc) { - return findInDoc(doc, isLineNextFn, false); -} - function willBreakFn(doc) { if (doc.type === "group" && doc.break) { return true; @@ -131,8 +131,10 @@ function breakParentGroup(groupStack) { const parentGroup = groupStack[groupStack.length - 1]; // Breaks are not propagated through conditional groups because // the user is expected to manually handle what breaks. - if (!parentGroup.expandedStates) { - parentGroup.break = true; + if (!parentGroup.expandedStates && !parentGroup.break) { + // An alternative truthy value allows to distinguish propagated group breaks + // and not to print them as `group(..., { break: true })` in `--debug-print-doc`. + parentGroup.break = "propagated"; } } return null; @@ -176,9 +178,12 @@ function removeLinesFn(doc) { // of breaking existing assumptions otherwise. if (doc.type === "line" && !doc.hard) { return doc.soft ? "" : " "; - } else if (doc.type === "if-break") { + } + + if (doc.type === "if-break") { return doc.flatContents || ""; } + return doc; } @@ -214,8 +219,10 @@ function stripDocTrailingHardlineFromDoc(doc) { switch (doc.type) { case "align": case "indent": + case "indent-if-break": case "group": - case "line-suffix": { + case "line-suffix": + case "label": { const contents = stripDocTrailingHardlineFromDoc(doc.contents); return { ...doc, contents }; } @@ -257,6 +264,7 @@ function cleanDocFn(doc) { break; case "align": case "indent": + case "indent-if-break": case "line-suffix": if (!doc.contents) { return ""; @@ -358,11 +366,7 @@ function normalizeDoc(doc) { function replaceNewlinesWithLiterallines(doc) { return mapDoc(doc, (currentDoc) => typeof currentDoc === "string" && currentDoc.includes("\n") - ? concat( - currentDoc - .split(/(\n)/g) - .map((v, i) => (i % 2 === 0 ? v : literalline)) - ) + ? currentDoc.split(/(\n)/g).map((v, i) => (i % 2 === 0 ? v : literalline)) : currentDoc ); } @@ -370,9 +374,7 @@ function replaceNewlinesWithLiterallines(doc) { module.exports = { isConcat, getDocParts, - isEmpty, willBreak, - isLineNext, traverseDoc, findInDoc, mapDoc, diff --git a/src/language-css/embed.js b/src/language-css/embed.js index 30bfde8af..19c7371c3 100644 --- a/src/language-css/embed.js +++ b/src/language-css/embed.js @@ -2,7 +2,7 @@ const { builders: { hardline }, } = require("../document"); -const { print: printFrontMatter } = require("../utils/front-matter"); +const printFrontMatter = require("../utils/front-matter/print"); function embed(path, print, textToDoc /*, options */) { const node = path.getValue(); diff --git a/src/language-css/parser-postcss.js b/src/language-css/parser-postcss.js index f8b7de6da..9c5144f02 100644 --- a/src/language-css/parser-postcss.js +++ b/src/language-css/parser-postcss.js @@ -1,7 +1,7 @@ "use strict"; const createError = require("../common/parser-create-error"); -const { parse: parseFrontMatter } = require("../utils/front-matter"); +const parseFrontMatter = require("../utils/front-matter/parse"); const { hasPragma } = require("./pragma"); const { hasSCSSInterpolation, @@ -71,7 +71,7 @@ function parseValueNode(valueNode, options) { for (let i = 0; i < groups.length; i++) { const group = groups[i]; if (group.type === "comma_group") { - groupList = groupList.concat(group.groups); + groupList = [...groupList, ...group.groups]; } else { groupList.push(group); } @@ -212,7 +212,7 @@ function parseValue(value, options) { try { result = valueParser(value, { loose: true }).parse(); - } catch (e) { + } catch { return { type: "value-unknown", value, @@ -246,7 +246,7 @@ function parseSelector(selector) { selectorParser((result_) => { result = result_; }).process(selector); - } catch (e) { + } catch { // Fail silently. It's better to print it as is than to try and parse it // Note: A common failure is for SCSS nested properties. `background: // none { color: red; }` is parsed as a NestedDeclaration by @@ -269,7 +269,7 @@ function parseMediaQuery(params) { try { result = mediaParser(params); - } catch (e) { + } catch { // Ignore bad media queries /* istanbul ignore next */ return { diff --git a/src/language-css/pragma.js b/src/language-css/pragma.js index 40d127e34..872f50d9c 100644 --- a/src/language-css/pragma.js +++ b/src/language-css/pragma.js @@ -1,7 +1,7 @@ "use strict"; const jsPragma = require("../language-js/pragma"); -const { parse: parseFrontMatter } = require("../utils/front-matter"); +const parseFrontMatter = require("../utils/front-matter/parse"); function hasPragma(text) { return jsPragma.hasPragma(parseFrontMatter(text).content); diff --git a/src/language-css/printer-postcss.js b/src/language-css/printer-postcss.js index 36fd35bb6..2e76e6b7c 100644 --- a/src/language-css/printer-postcss.js +++ b/src/language-css/printer-postcss.js @@ -55,6 +55,7 @@ const { hasParensAroundNode, hasEmptyRawBefore, isKeyValuePairNode, + isKeyInValuePairNode, isDetachedRulesetCallNode, isTemplatePlaceholderNode, isTemplatePropNode, @@ -187,7 +188,7 @@ function genericPrint(path, options, print) { options.originalText[locEnd(node) - 1] !== ";" ? "" : options.__isHTMLStyleAttribute && isLastNode(path, node) - ? ifBreak(";", "") + ? ifBreak(";") : ";", ]; } @@ -865,8 +866,9 @@ function genericPrint(path, options, print) { const lastItem = node.groups[node.groups.length - 1]; const isLastItemComment = lastItem && lastItem.type === "value-comment"; + const isKey = isKeyInValuePairNode(node, parentNode); - return group( + const printed = group( [ node.open ? path.call(print, "open") : "", indent([ @@ -882,6 +884,7 @@ function genericPrint(path, options, print) { isKeyValuePairNode(node) && node.type === "value-comma_group" && node.groups && + node.groups[0].type !== "value-paren_group" && node.groups[2] && node.groups[2].type === "value-paren_group" ) { @@ -907,9 +910,11 @@ function genericPrint(path, options, print) { node.close ? path.call(print, "close") : "", ], { - shouldBreak: isSCSSMapItem, + shouldBreak: isSCSSMapItem && !isKey, } ); + + return isKey ? dedent(printed) : printed; } case "value-func": { return [ @@ -943,7 +948,9 @@ function genericPrint(path, options, print) { return [ node.value, // Don't add spaces on escaped colon `:`, e.g: grid-template-rows: [row-1-00\:00] auto; - (prevNode && prevNode.value[prevNode.value.length - 1] === "\\") || + (prevNode && + typeof prevNode.value === "string" && + prevNode.value[prevNode.value.length - 1] === "\\") || // Don't add spaces on `:` in `url` function (i.e. `url(fbglyph: cross-outline, fig-white)`) insideValueFunctionNode(path, "url") ? "" diff --git a/src/language-css/utils.js b/src/language-css/utils.js index abaa1e620..a68b39f15 100644 --- a/src/language-css/utils.js +++ b/src/language-css/utils.js @@ -30,7 +30,7 @@ const colorAdjusterFunctions = new Set([ ]); function getAncestorCounter(path, typeOrTypes) { - const types = [].concat(typeOrTypes); + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; let counter = -1; let ancestorNode; @@ -146,7 +146,9 @@ function insideICSSRuleNode(path) { } function insideAtRuleNode(path, atRuleNameOrAtRuleNames) { - const atRuleNames = [].concat(atRuleNameOrAtRuleNames); + const atRuleNames = Array.isArray(atRuleNameOrAtRuleNames) + ? atRuleNameOrAtRuleNames + : [atRuleNameOrAtRuleNames]; const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); return ( @@ -407,7 +409,22 @@ function isWordNode(node) { } function isColonNode(node) { - return node.type === "value-colon"; + return node && node.type === "value-colon"; +} + +function isKeyInValuePairNode(node, parentNode) { + if (!isKeyValuePairNode(parentNode)) { + return false; + } + + const { groups } = parentNode; + const index = groups.indexOf(node); + + if (index === -1) { + return false; + } + + return isColonNode(groups[index + 1]); } function isMediaAndSupportsKeywords(node) { @@ -502,6 +519,7 @@ module.exports = { isPostcssSimpleVarNode, isKeyValuePairNode, isKeyValuePairInParenGroupNode, + isKeyInValuePairNode, isSCSSMapItemNode, isInlineValueCommentNode, isHashNode, diff --git a/src/language-graphql/printer-graphql.js b/src/language-graphql/printer-graphql.js index e25367de4..c39e67ae4 100644 --- a/src/language-graphql/printer-graphql.js +++ b/src/language-graphql/printer-graphql.js @@ -35,7 +35,7 @@ function genericPrint(path, options, print) { } case "OperationDefinition": { const hasOperation = options.originalText[locStart(n)] !== "{"; - const hasName = !!n.name; + const hasName = Boolean(n.name); return [ hasOperation ? n.operation : "", hasOperation && hasName ? [" ", path.call(print, "name")] : "", diff --git a/src/language-handlebars/clean.js b/src/language-handlebars/clean.js index db7b843ff..7c3e12721 100644 --- a/src/language-handlebars/clean.js +++ b/src/language-handlebars/clean.js @@ -1,15 +1,21 @@ "use strict"; function clean(ast, newNode /*, parent*/) { - // (Glimmer/HTML) ignore TextNode whitespace + // (Glimmer/HTML) ignore TextNode if (ast.type === "TextNode") { const trimmed = ast.chars.trim(); if (!trimmed) { return null; } - newNode.chars = trimmed; + newNode.chars = trimmed.replace(/[\t\n\f\r ]+/g, " "); + } + + // `class` is reformatted + if (ast.type === "AttrNode" && ast.name.toLowerCase() === "class") { + delete newNode.value; } } + clean.ignoredProperties = new Set(["loc", "selfClosing"]); module.exports = clean; diff --git a/src/language-handlebars/printer-glimmer.js b/src/language-handlebars/printer-glimmer.js index 50092ce7f..1eef2f20f 100644 --- a/src/language-handlebars/printer-glimmer.js +++ b/src/language-handlebars/printer-glimmer.js @@ -1,11 +1,21 @@ "use strict"; const { - builders: { dedent, group, hardline, ifBreak, indent, join, line, softline }, + builders: { + dedent, + fill, + group, + hardline, + ifBreak, + indent, + join, + line, + softline, + literalline, + }, utils: { getDocParts }, } = require("../document"); -const { isNonEmptyArray } = require("../common/util"); - +const { isNonEmptyArray, replaceEndOfLineWith } = require("../common/util"); const { locStart, locEnd } = require("./loc"); const clean = require("./clean"); const { @@ -21,6 +31,8 @@ const { isWhitespaceNode, } = require("./utils"); +const NEWLINES_TO_PRESERVE_MAX = 2; + // Formatter based on @glimmerjs/syntax's built-in test formatter: // https://github.com/glimmerjs/glimmer-vm/blob/master/packages/%40glimmer/syntax/lib/generation/print.ts @@ -47,7 +59,7 @@ function print(path, options, print) { const startingTag = group(printStartingTag(path, print)); const escapeNextElementNode = - options.htmlWhitespaceSensitivity !== "strict" && + options.htmlWhitespaceSensitivity === "ignore" && isNextNodeOfSomeType(path, ["ElementNode"]) ? softline : ""; @@ -62,7 +74,7 @@ function print(path, options, print) { return [startingTag, indent(endingTag), escapeNextElementNode]; } - if (options.htmlWhitespaceSensitivity !== "strict") { + if (options.htmlWhitespaceSensitivity === "ignore") { return [ startingTag, indent(printChildren(path, options, print)), @@ -152,23 +164,34 @@ function print(path, options, print) { if (isEmptyText && locStart(n.value) === locEnd(n.value)) { return n.name; } - const value = path.call(print, "value"); - const quotedValue = isText - ? printStringLiteral( - typeof value === "string" ? value : getDocParts(value).join(), - options - ) - : value; - return [n.name, "=", quotedValue]; + + // Let's assume quotes inside the content of text nodes are already + // properly escaped with entities, otherwise the parse wouldn't have parsed them. + const quote = isText + ? chooseEnclosingQuote(options, n.value.chars).quote + : n.value.type === "ConcatStatement" + ? chooseEnclosingQuote( + options, + n.value.parts + .filter((part) => part.type === "TextNode") + .map((part) => part.chars) + .join("") + ).quote + : ""; + + const valueDoc = path.call(print, "value"); + + return [ + n.name, + "=", + quote, + n.name === "class" && quote ? group(indent(valueDoc)) : valueDoc, + quote, + ]; } case "ConcatStatement": { - const quote = options.singleQuote ? "'" : '"'; - return [ - quote, - ...path.map((partPath) => print(partPath), "parts"), - quote, - ]; + return path.map(print, "parts"); } case "Hash": { @@ -178,20 +201,74 @@ function print(path, options, print) { return [n.key, "=", path.call(print, "value")]; } case "TextNode": { - const inAttrNode = path.stack.includes("attributes"); + /* if `{{my-component}}` (or any text containing "{{") + * makes it to the TextNode, it means it was escaped, + * so let's print it escaped, ie.; `\{{my-component}}` */ + let text = n.chars.replace(/{{/g, "\\{{"); - if (options.htmlWhitespaceSensitivity === "strict" && !inAttrNode) { + const attrName = getCurrentAttributeName(path); + + if (attrName) { + // TODO: format style and srcset attributes + if (attrName === "class") { + const formattedClasses = text.trim().split(/\s+/).join(" "); + + let leadingSpace = false; + let trailingSpace = false; + + if (isParentOfSomeType(path, ["ConcatStatement"])) { + if ( + isPreviousNodeOfSomeType(path, ["MustacheStatement"]) && + /^\s/.test(text) + ) { + leadingSpace = true; + } + if ( + isNextNodeOfSomeType(path, ["MustacheStatement"]) && + /\s$/.test(text) && + formattedClasses !== "" + ) { + trailingSpace = true; + } + } + + return [ + leadingSpace ? line : "", + formattedClasses, + trailingSpace ? line : "", + ]; + } + + return replaceEndOfLineWith(text, literalline); + } + + const whitespacesOnlyRE = /^[\t\n\f\r ]*$/; + const isWhitespaceOnly = whitespacesOnlyRE.test(text); + const isFirstElement = !getPreviousNode(path); + const isLastElement = !getNextNode(path); + + if (options.htmlWhitespaceSensitivity !== "ignore") { // https://infra.spec.whatwg.org/#ascii-whitespace const leadingWhitespacesRE = /^[\t\n\f\r ]*/; const trailingWhitespacesRE = /[\t\n\f\r ]*$/; - const whitespacesOnlyRE = /^[\t\n\f\r ]*$/; - if (whitespacesOnlyRE.test(n.chars)) { + // let's remove the file's final newline + // https://github.com/ember-cli/ember-new-output/blob/1a04c67ddd02ccb35e0ff41bb5cbce34b31173ef/.editorconfig#L16 + const shouldTrimTrailingNewlines = + isLastElement && isParentOfSomeType(path, ["Template"]); + const shouldTrimLeadingNewlines = + isFirstElement && isParentOfSomeType(path, ["Template"]); + + if (isWhitespaceOnly) { + if (shouldTrimLeadingNewlines || shouldTrimTrailingNewlines) { + return ""; + } + let breaks = [line]; - const newlines = countNewLines(n.chars); + const newlines = countNewLines(text); if (newlines) { - breaks = generateHardlines(newlines, 2); + breaks = generateHardlines(newlines); } if (isLastNodeOfSiblings(path)) { @@ -201,10 +278,8 @@ function print(path, options, print) { return breaks; } - const [lead] = n.chars.match(leadingWhitespacesRE); - const [tail] = n.chars.match(trailingWhitespacesRE); - - let text = n.chars; + const [lead] = text.match(leadingWhitespacesRE); + const [tail] = text.match(trailingWhitespacesRE); let leadBreaks = []; if (lead) { @@ -212,7 +287,7 @@ function print(path, options, print) { const leadingNewlines = countNewLines(lead); if (leadingNewlines) { - leadBreaks = generateHardlines(countNewLines(lead) || 1, 2); + leadBreaks = generateHardlines(leadingNewlines); } text = text.replace(leadingWhitespacesRE, ""); @@ -220,11 +295,13 @@ function print(path, options, print) { let trailBreaks = []; if (tail) { - trailBreaks = [line]; + if (!shouldTrimTrailingNewlines) { + trailBreaks = [line]; - const trailingNewlines = countNewLines(tail); - if (trailingNewlines) { - trailBreaks = generateHardlines(trailingNewlines || 1, 2); + const trailingNewlines = countNewLines(tail); + if (trailingNewlines) { + trailBreaks = generateHardlines(trailingNewlines); + } if (isLastNodeOfSiblings(path)) { trailBreaks = trailBreaks.map((hardline) => dedent(hardline)); @@ -234,17 +311,13 @@ function print(path, options, print) { text = text.replace(trailingWhitespacesRE, ""); } - return [...leadBreaks, text, ...trailBreaks]; + return [...leadBreaks, fill(getTextValueParts(text)), ...trailBreaks]; } - const maxLineBreaksToPreserve = 2; - const isFirstElement = !getPreviousNode(path); - const isLastElement = !getNextNode(path); - const isWhitespaceOnly = !/\S/.test(n.chars); - const lineBreaksCount = countNewLines(n.chars); + const lineBreaksCount = countNewLines(text); - let leadingLineBreaksCount = countLeadingNewLines(n.chars); - let trailingLineBreaksCount = countTrailingNewLines(n.chars); + let leadingLineBreaksCount = countLeadingNewLines(text); + let trailingLineBreaksCount = countTrailingNewLines(text); if ( (isFirstElement || isLastElement) && @@ -257,7 +330,7 @@ function print(path, options, print) { if (isWhitespaceOnly && lineBreaksCount) { leadingLineBreaksCount = Math.min( lineBreaksCount, - maxLineBreaksToPreserve + NEWLINES_TO_PRESERVE_MAX ); trailingLineBreaksCount = 0; } else { @@ -270,34 +343,6 @@ function print(path, options, print) { } } - if (inAttrNode) { - // TODO: format style and srcset attributes - if (!isInAttributeOfName(path, "class")) { - return n.chars; - } - - let leadingSpace = ""; - let trailingSpace = ""; - - if (isParentOfSomeType(path, ["ConcatStatement"])) { - if (isPreviousNodeOfSomeType(path, ["MustacheStatement"])) { - leadingSpace = " "; - } - if (isNextNodeOfSomeType(path, ["MustacheStatement"])) { - trailingSpace = " "; - } - } - - return [ - ...generateHardlines(leadingLineBreaksCount, maxLineBreaksToPreserve), - n.chars.replace(/^\s+/g, leadingSpace).replace(/\s+$/, trailingSpace), - ...generateHardlines( - trailingLineBreaksCount, - maxLineBreaksToPreserve - ), - ]; - } - let leadingSpace = ""; let trailingSpace = ""; @@ -325,19 +370,14 @@ function print(path, options, print) { trailingSpace = ""; } - let text = n.chars; - /* if `{{my-component}}` (or any text starting with a mustache) - * makes it to the TextNode, - * it means it was escaped, - * so let's print it escaped, ie.; `\{{my-component}}` */ - if (text.startsWith("{{") && text.includes("}}")) { - text = "\\" + text; - } + text = text + .replace(/^[\t\n\f\r ]+/g, leadingSpace) + .replace(/[\t\n\f\r ]+$/, trailingSpace); return [ - ...generateHardlines(leadingLineBreaksCount, maxLineBreaksToPreserve), - text.replace(/^\s+/g, leadingSpace).replace(/\s+$/, trailingSpace), - ...generateHardlines(trailingLineBreaksCount, maxLineBreaksToPreserve), + ...generateHardlines(leadingLineBreaksCount), + fill(getTextValueParts(text)), + ...generateHardlines(trailingLineBreaksCount), ]; } case "MustacheCommentStatement": { @@ -395,26 +435,14 @@ function print(path, options, print) { function printStartingTag(path, print) { const node = path.getValue(); - const attributesLike = []; - if (isNonEmptyArray(node.attributes)) { - const attributes = join(line, path.map(print, "attributes")); - attributesLike.push(line, attributes); - } - - if (isNonEmptyArray(node.modifiers)) { - const modifiers = join(line, path.map(print, "modifiers")); - attributesLike.push(line, modifiers); - } - - if (isNonEmptyArray(node.comments)) { - const comments = join(line, path.map(print, "comments")); - attributesLike.push(line, comments); - } - - if (isNonEmptyArray(node.blockParams)) { - const blockParams = printBlockParams(node); - attributesLike.push(line, blockParams); - } + const attributesLike = ["attributes", "modifiers", "comments", "blockParams"] + .filter((property) => isNonEmptyArray(node[property])) + .map((property) => [ + line, + property === "blockParams" + ? printBlockParams(node) + : join(line, path.map(print, property)), + ]); return [ "<", @@ -427,14 +455,14 @@ function printStartingTag(path, print) { function printChildren(path, options, print) { const node = path.getValue(); const isEmpty = node.children.every((n) => isWhitespaceNode(n)); - if (options.htmlWhitespaceSensitivity !== "strict" && isEmpty) { + if (options.htmlWhitespaceSensitivity === "ignore" && isEmpty) { return ""; } return path.map((childPath, childIndex) => { const printedChild = print(childPath, options, print); - if (childIndex === 0 && options.htmlWhitespaceSensitivity !== "strict") { + if (childIndex === 0 && options.htmlWhitespaceSensitivity === "ignore") { return [softline, printedChild]; } @@ -530,7 +558,7 @@ function printOpenBlock(path, print) { function printElseBlock(node, options) { return [ - options.htmlWhitespaceSensitivity !== "strict" ? hardline : "", + options.htmlWhitespaceSensitivity === "ignore" ? hardline : "", printInverseBlockOpeningMustache(node), "else", printInverseBlockClosingMustache(node), @@ -551,7 +579,7 @@ function printElseIfBlock(path, print) { function printCloseBlock(path, print, options) { const node = path.getValue(); - if (options.htmlWhitespaceSensitivity !== "strict") { + if (options.htmlWhitespaceSensitivity === "ignore") { const escape = blockStatementHasOnlyWhitespaceInProgram(node) ? softline : hardline; @@ -600,7 +628,7 @@ function printProgram(path, print, options) { const program = path.call(print, "program"); - if (options.htmlWhitespaceSensitivity !== "strict") { + if (options.htmlWhitespaceSensitivity === "ignore") { return indent([hardline, program]); } @@ -612,7 +640,7 @@ function printInverse(path, print, options) { const inverse = path.call(print, "inverse"); const printed = - options.htmlWhitespaceSensitivity !== "strict" + options.htmlWhitespaceSensitivity === "ignore" ? [hardline, inverse] : inverse; @@ -629,13 +657,21 @@ function printInverse(path, print, options) { /* TextNode print helpers */ -function isInAttributeOfName(path, type) { - return ( - (isParentOfSomeType(path, ["AttrNode"]) && - path.getParentNode().name.toLowerCase() === type) || - (isParentOfSomeType(path, ["ConcatStatement"]) && - path.getParentNode(1).name.toLowerCase() === type) - ); +function getTextValueParts(value) { + return getDocParts(join(line, splitByHtmlWhitespace(value))); +} + +function splitByHtmlWhitespace(string) { + return string.split(/[\t\n\f\r ]+/); +} + +function getCurrentAttributeName(path) { + for (let depth = 0; depth < 2; depth++) { + const parentNode = path.getParentNode(depth); + if (parentNode && parentNode.type === "AttrNode") { + return parentNode.name.toLowerCase(); + } + } } function countNewLines(string) { @@ -658,8 +694,8 @@ function countTrailingNewLines(string) { return countNewLines(newLines); } -function generateHardlines(number = 0, max = 0) { - return new Array(Math.min(number, max)).fill(hardline); +function generateHardlines(number = 0) { + return new Array(Math.min(number, NEWLINES_TO_PRESERVE_MAX)).fill(hardline); } /* StringLiteral print helpers */ @@ -674,6 +710,11 @@ function generateHardlines(number = 0, max = 0) { * @param {object} options - the prettier options object */ function printStringLiteral(stringLiteral, options) { + const { quote, regex } = chooseEnclosingQuote(options, stringLiteral); + return [quote, stringLiteral.replace(regex, `\\${quote}`), quote]; +} + +function chooseEnclosingQuote(options, stringLiteral) { const double = { quote: '"', regex: /"/g }; const single = { quote: "'", regex: /'/g }; @@ -697,13 +738,7 @@ function printStringLiteral(stringLiteral, options) { shouldUseAlternateQuote = numPreferredQuotes > numAlternateQuotes; } - const enclosingQuote = shouldUseAlternateQuote ? alternate : preferred; - const escapedStringLiteral = stringLiteral.replace( - enclosingQuote.regex, - `\\${enclosingQuote.quote}` - ); - - return [enclosingQuote.quote, escapedStringLiteral, enclosingQuote.quote]; + return shouldUseAlternateQuote ? alternate : preferred; } /* SubExpression print helpers */ diff --git a/src/language-handlebars/utils.js b/src/language-handlebars/utils.js index 7b41711cb..963f561ca 100644 --- a/src/language-handlebars/utils.js +++ b/src/language-handlebars/utils.js @@ -49,7 +49,7 @@ function isWhitespaceNode(node) { } function isNodeOfSomeType(node, types) { - return node && types.some((type) => node.type === type); + return node && types.includes(node.type); } function isParentOfSomeType(path, types) { diff --git a/src/language-html/ast.js b/src/language-html/ast.js index f612f67be..1f57014ab 100644 --- a/src/language-html/ast.js +++ b/src/language-html/ast.js @@ -1,5 +1,6 @@ "use strict"; +const fromPairs = require("lodash/fromPairs"); const { isNonEmptyArray } = require("../common/util"); const NODES_KEYS = { attrs: true, @@ -8,8 +9,7 @@ const NODES_KEYS = { class Node { constructor(props = {}) { - for (const key of Object.keys(props)) { - const value = props[key]; + for (const [key, value] of Object.entries(props)) { if (key in NODES_KEYS) { this._setNodes(key, value); } else { @@ -23,10 +23,9 @@ class Node { this[key] = cloneAndUpdateNodes(nodes, this); if (key === "attrs") { setNonEnumerableProperties(this, { - attrMap: this[key].reduce((reduced, attr) => { - reduced[attr.fullName] = attr.value; - return reduced; - }, Object.create(null)), + attrMap: fromPairs( + this[key].map((attr) => [attr.fullName, attr.value]) + ), }); } } @@ -123,10 +122,13 @@ function cloneAndUpdateNodes(nodes, parent) { } function setNonEnumerableProperties(obj, props) { - const descriptors = Object.keys(props).reduce((reduced, key) => { - reduced[key] = { value: props[key], enumerable: false }; - return reduced; - }, {}); + const descriptors = fromPairs( + Object.entries(props).map(([key, value]) => [ + key, + { value, enumerable: false }, + ]) + ); + Object.defineProperties(obj, descriptors); } diff --git a/src/language-html/conditional-comment.js b/src/language-html/conditional-comment.js index 228076446..0bb2ce47a 100644 --- a/src/language-html/conditional-comment.js +++ b/src/language-html/conditional-comment.js @@ -39,7 +39,7 @@ function parseIeConditionalStartEndComment(node, parseHtml, match) { const [complete, children] = (() => { try { return [true, parseHtml(data, contentStartSpan).children]; - } catch (e) { + } catch { const text = { type: "text", value: data, diff --git a/src/language-html/index.js b/src/language-html/index.js index 61882c524..1caf94c65 100644 --- a/src/language-html/index.js +++ b/src/language-html/index.js @@ -17,9 +17,10 @@ const languages = [ since: "1.15.0", parsers: ["html"], vscodeLanguageIds: ["html"], - extensions: data.extensions.concat([ + extensions: [ + ...data.extensions, ".mjml", // MJML is considered XML in Linguist but it should be formatted as HTML - ]), + ], })), createLanguage(require("linguist-languages/data/HTML.json"), () => ({ name: "Lightning Web Components", diff --git a/src/language-html/parser-html.js b/src/language-html/parser-html.js index 748c3cf7a..079e99760 100644 --- a/src/language-html/parser-html.js +++ b/src/language-html/parser-html.js @@ -5,7 +5,7 @@ const { ParseLocation, ParseSourceFile, } = require("angular-html-parser/lib/compiler/src/parse_util"); -const { parse: parseFrontMatter } = require("../utils/front-matter"); +const parseFrontMatter = require("../utils/front-matter/parse"); const createError = require("../common/parser-create-error"); const { inferParserByLanguage } = require("../common/util"); const { @@ -74,10 +74,7 @@ function ngHtmlParser( } const langAttr = node.attrs.find((attr) => attr.name === "lang"); const langValue = langAttr && langAttr.value; - return ( - langValue == null || - inferParserByLanguage(langValue, options) === "html" - ); + return !langValue || inferParserByLanguage(langValue, options) === "html"; }; if (rootNodes.some(shouldParseAsHTML)) { let secondParseResult; diff --git a/src/language-html/print-preprocess.js b/src/language-html/print-preprocess.js index 5a790780f..896d72bcd 100644 --- a/src/language-html/print-preprocess.js +++ b/src/language-html/print-preprocess.js @@ -331,7 +331,7 @@ function extractWhitespaces(ast /*, options*/) { // extract whitespace nodes .reduce((newChildren, child) => { if (child.type !== "text" || isWhitespaceSensitive) { - return newChildren.concat(child); + return [...newChildren, child]; } const localChildren = []; @@ -361,7 +361,7 @@ function extractWhitespaces(ast /*, options*/) { localChildren.push({ type: TYPE_WHITESPACE }); } - return newChildren.concat(localChildren); + return [...newChildren, ...localChildren]; }, []) // set hasLeadingSpaces/hasTrailingSpaces and filter whitespace nodes .reduce((newChildren, child, i, children) => { @@ -375,11 +375,14 @@ function extractWhitespaces(ast /*, options*/) { i !== children.length - 1 && children[i + 1].type === TYPE_WHITESPACE; - return newChildren.concat({ - ...child, - hasLeadingSpaces, - hasTrailingSpaces, - }); + return [ + ...newChildren, + { + ...child, + hasLeadingSpaces, + hasTrailingSpaces, + }, + ]; }, []), }); }); diff --git a/src/language-html/printer-html.js b/src/language-html/printer-html.js index d4863e233..f6efb5b72 100644 --- a/src/language-html/printer-html.js +++ b/src/language-html/printer-html.js @@ -9,16 +9,17 @@ const { group, hardline, ifBreak, + indentIfBreak, indent, join, line, literalline, softline, }, - utils: { mapDoc, cleanDoc, getDocParts }, + utils: { mapDoc, cleanDoc, getDocParts, isConcat }, } = require("../document"); const { replaceEndOfLineWith, isNonEmptyArray } = require("../common/util"); -const { print: printFrontMatter } = require("../utils/front-matter"); +const printFrontMatter = require("../utils/front-matter/print"); const clean = require("./clean"); const { htmlTrimPreserveIndentation, @@ -77,7 +78,7 @@ function embed(path, print, textToDoc, options) { if (!isEmpty) { doc = textToDoc( htmlTrimPreserveIndentation(content), - { parser }, + { parser, __embeddedInHtml: true }, { stripTrailingHardline: true } ); isEmpty = doc === ""; @@ -103,7 +104,7 @@ function embed(path, print, textToDoc, options) { parser === "markdown" ? dedentString(node.value.replace(/^[^\S\n]*?\n/, "")) : node.value; - const textToDocOptions = { parser }; + const textToDocOptions = { parser, __embeddedInHtml: true }; if (options.parser === "html" && parser === "babel") { let sourceType = "script"; const { attrMap } = node.parent; @@ -127,21 +128,24 @@ function embed(path, print, textToDoc, options) { ]; } } else if (node.parent.type === "interpolation") { + const textToDocOptions = { + __isInHtmlInterpolation: true, // to avoid unexpected `}}` + __embeddedInHtml: true, + }; + if (options.parser === "angular") { + textToDocOptions.parser = "__ng_interpolation"; + textToDocOptions.trailingComma = "none"; + } else if (options.parser === "vue") { + textToDocOptions.parser = "__vue_expression"; + } else { + textToDocOptions.parser = "__js_expression"; + } return [ indent([ line, - textToDoc( - node.value, - { - __isInHtmlInterpolation: true, // to avoid unexpected `}}` - ...(options.parser === "angular" - ? { parser: "__ng_interpolation", trailingComma: "none" } - : options.parser === "vue" - ? { parser: "__vue_expression" } - : { parser: "__js_expression" }), - }, - { stripTrailingHardline: true } - ), + textToDoc(node.value, textToDocOptions, { + stripTrailingHardline: true, + }), ]), node.parent.next && needsToBorrowPrevClosingTagEndMarker(node.parent.next) @@ -189,7 +193,7 @@ function embed(path, print, textToDoc, options) { // strictly prefer single quote to avoid unnecessary html entity escape textToDoc( code, - { __isInHtmlAttribute: true, ...opts }, + { __isInHtmlAttribute: true, __embeddedInHtml: true, ...opts }, { stripTrailingHardline: true } ), options @@ -228,13 +232,13 @@ function genericPrint(path, options, print) { case "element": case "ieConditionalComment": { if (shouldPreserveContent(node, options)) { - return [].concat( + return [ printOpeningTagPrefix(node, options), group(printOpeningTag(path, options, print)), - replaceEndOfLineWith(getNodeContent(node, options), literalline), - printClosingTag(node, options), - printClosingTagSuffix(node, options) - ); + ...replaceEndOfLineWith(getNodeContent(node, options), literalline), + ...printClosingTag(node, options), + printClosingTagSuffix(node, options), + ]; } /** * do not break: @@ -275,9 +279,7 @@ function genericPrint(path, options, print) { forceBreakContent(node) ? breakParent : "", ((childrenDoc) => shouldHugContent - ? ifBreak(indent(childrenDoc), childrenDoc, { - groupId: attrGroupId, - }) + ? indentIfBreak(childrenDoc, { groupId: attrGroupId }) : (isScriptLikeTag(node) || isVueCustomBlock(node, options)) && node.parent.type === "root" && @@ -367,7 +369,11 @@ function genericPrint(path, options, print) { ...getTextValueParts(node), printClosingTagSuffix(node, options), ]); - return typeof printed === "string" ? printed : fill(getDocParts(printed)); + if (isConcat(printed) || printed.type === "fill") { + return fill(getDocParts(printed)); + } + /* istanbul ignore next */ + return printed; } case "docType": return [ @@ -505,25 +511,25 @@ function printChildren(path, options, print) { } } - return [].concat( - prevParts, + return [ + ...prevParts, group([ ...leadingParts, group([printChild(childPath), ...trailingParts], { id: groupIds[childIndex], }), ]), - nextParts - ); + ...nextParts, + ]; }, "children"); function printChild(childPath) { const child = childPath.getValue(); if (hasPrettierIgnore(child)) { - return [].concat( + return [ printOpeningTagPrefix(child, options), - replaceEndOfLineWith( + ...replaceEndOfLineWith( options.originalText.slice( locStart(child) + (child.prev && needsToBorrowNextOpeningTagStartMarker(child.prev) @@ -536,8 +542,8 @@ function printChildren(path, options, print) { ), literalline ), - printClosingTagSuffix(child, options) - ); + printClosingTagSuffix(child, options), + ]; } return print(childPath); @@ -989,7 +995,7 @@ function printEmbeddedAttributeValue(node, originalTextToDoc, options) { const textToDoc = (code, opts) => originalTextToDoc( code, - { __onHtmlBindingRoot, ...opts }, + { __onHtmlBindingRoot, __embeddedInHtml: true, ...opts }, { stripTrailingHardline: true } ); @@ -1166,7 +1172,7 @@ function printEmbeddedAttributeValue(node, originalTextToDoc, options) { "}}", ]) ); - } catch (e) { + } catch { parts.push("{{", replaceEndOfLineWith(part, literalline), "}}"); } } diff --git a/src/language-html/syntax-vue.js b/src/language-html/syntax-vue.js index 0ce5bb005..897808d7d 100644 --- a/src/language-html/syntax-vue.js +++ b/src/language-html/syntax-vue.js @@ -64,14 +64,10 @@ function parseVueFor(value) { } function printVueBindings(value, textToDoc) { - return textToDoc( - `function _(${value}) {}`, - { - parser: "babel", - __isVueBindings: true, - }, - { stripTrailingHardline: true } - ); + return textToDoc(`function _(${value}) {}`, { + parser: "babel", + __isVueBindings: true, + }); } function isVueEventBindingExpression(eventBindingValue) { diff --git a/src/language-html/utils.js b/src/language-html/utils.js index e4ac2446f..06b85e362 100644 --- a/src/language-html/utils.js +++ b/src/language-html/utils.js @@ -46,8 +46,8 @@ function arrayToMap(array) { function mapObject(object, fn) { const newObject = Object.create(null); - for (const key of Object.keys(object)) { - newObject[key] = fn(object[key], key); + for (const [key, value] of Object.entries(object)) { + newObject[key] = fn(value, key); } return newObject; } diff --git a/src/language-js/clean.js b/src/language-js/clean.js index 5604e5b7b..903988eb6 100644 --- a/src/language-js/clean.js +++ b/src/language-js/clean.js @@ -18,6 +18,12 @@ const ignoredProperties = new Set([ "tokens", ]); +const removeTemplateElementsValue = (node) => { + for (const templateElement of node.quasis) { + delete templateElement.value; + } +}; + function clean(ast, newObj, parent) { if (ast.type === "Program") { delete newObj.sourceType; @@ -82,11 +88,6 @@ function clean(ast, newObj, parent) { delete newObj.key; } - if (ast.type === "OptionalMemberExpression" && ast.optional === false) { - newObj.type = "MemberExpression"; - delete newObj.optional; - } - // Remove raw and cooked values from TemplateElement when it's CSS // styled-jsx if ( @@ -94,21 +95,13 @@ function clean(ast, newObj, parent) { ast.openingElement.name.name === "style" && ast.openingElement.attributes.some((attr) => attr.name.name === "jsx") ) { - const templateLiterals = newObj.children - .filter( - (child) => - child.type === "JSXExpressionContainer" && - child.expression.type === "TemplateLiteral" - ) - .map((container) => container.expression); - - const quasis = templateLiterals.reduce( - (quasis, templateLiteral) => quasis.concat(templateLiteral.quasis), - [] - ); - - for (const q of quasis) { - delete q.value; + for (const { type, expression } of newObj.children) { + if ( + type === "JSXExpressionContainer" && + expression.type === "TemplateLiteral" + ) { + removeTemplateElementsValue(expression); + } } } @@ -119,9 +112,7 @@ function clean(ast, newObj, parent) { ast.value.type === "JSXExpressionContainer" && ast.value.expression.type === "TemplateLiteral" ) { - for (const q of newObj.value.expression.quasis) { - delete q.value; - } + removeTemplateElementsValue(newObj.value.expression); } // We change quotes @@ -147,26 +138,18 @@ function clean(ast, newObj, parent) { index, prop, ] of newObj.expression.arguments[0].properties.entries()) { - let templateLiteral = null; - switch (astProps[index].key.name) { case "styles": if (prop.value.type === "ArrayExpression") { - templateLiteral = prop.value.elements[0]; + removeTemplateElementsValue(prop.value.elements[0]); } break; case "template": if (prop.value.type === "TemplateLiteral") { - templateLiteral = prop.value; + removeTemplateElementsValue(prop.value); } break; } - - if (templateLiteral) { - for (const q of templateLiteral.quasis) { - delete q.value; - } - } } } @@ -183,9 +166,7 @@ function clean(ast, newObj, parent) { ast.tag.name === "html")) || ast.tag.type === "CallExpression") ) { - for (const quasi of newObj.quasi.quasis) { - delete quasi.value; - } + removeTemplateElementsValue(newObj.quasi); } if (ast.type === "TemplateLiteral") { // This checks for a leading comment that is exactly `/* GraphQL */` @@ -204,21 +185,12 @@ function clean(ast, newObj, parent) { ); if ( hasLanguageComment || - (parent.type === "CallExpression" && parent.callee.name === "graphql") + (parent.type === "CallExpression" && parent.callee.name === "graphql") || + // TODO: check parser + // `flow` and `typescript` don't have `leadingComments` + !ast.leadingComments ) { - for (const quasi of newObj.quasis) { - delete quasi.value; - } - } - - // TODO: check parser - // `flow` and `typescript` don't have `leadingComments` - if (!ast.leadingComments) { - for (const quasi of newObj.quasis) { - if (quasi.value) { - delete quasi.value.cooked; - } - } + removeTemplateElementsValue(newObj); } } @@ -226,10 +198,12 @@ function clean(ast, newObj, parent) { newObj.value = newObj.value.trimEnd(); } - // TODO: Remove this when fixing #9760 - if (ast.type === "ClassMethod") { - delete newObj.declare; - delete newObj.readonly; + // Prettier removes degenerate union and intersection types with only one member. + if ( + (ast.type === "TSIntersectionType" || ast.type === "TSUnionType") && + ast.types.length === 1 + ) { + return newObj.types[0]; } } diff --git a/src/language-js/comments.js b/src/language-js/comments.js index e4e0caa09..d40b113e4 100644 --- a/src/language-js/comments.js +++ b/src/language-js/comments.js @@ -20,13 +20,15 @@ const { hasFlowShorthandAnnotationComment, hasFlowAnnotationComment, hasIgnoreComment, + isCallExpression, + isMemberExpression, } = require("./utils"); const { locStart, locEnd } = require("./loc"); /** * @typedef {import("./types/estree").Node} Node * @typedef {import("./types/estree").Comment} Comment - * @typedef {import("../common/fast-path")} FastPath + * @typedef {import("../common/ast-path")} AstPath * * @typedef {Object} CommentContext * @property {Comment} comment @@ -315,9 +317,7 @@ function handleMemberExpressionComments({ followingNode, }) { if ( - enclosingNode && - (enclosingNode.type === "MemberExpression" || - enclosingNode.type === "OptionalMemberExpression") && + isMemberExpression(enclosingNode) && followingNode && followingNode.type === "Identifier" ) { @@ -521,8 +521,7 @@ function handleCommentInEmptyParens({ comment, enclosingNode, text }) { enclosingNode && ((isRealFunctionLikeNode(enclosingNode) && getFunctionParameters(enclosingNode).length === 0) || - ((enclosingNode.type === "CallExpression" || - enclosingNode.type === "OptionalCallExpression" || + ((isCallExpression(enclosingNode) || enclosingNode.type === "NewExpression") && enclosingNode.arguments.length === 0)) ) { @@ -644,9 +643,7 @@ function handleCallExpressionComments({ enclosingNode, }) { if ( - enclosingNode && - (enclosingNode.type === "CallExpression" || - enclosingNode.type === "OptionalCallExpression") && + isCallExpression(enclosingNode) && precedingNode && enclosingNode.callee === precedingNode && enclosingNode.arguments.length > 0 @@ -713,7 +710,9 @@ function handleOnlyComments({ comment, enclosingNode, ast, isLastComment }) { addLeadingComment(ast, comment); } return true; - } else if ( + } + + if ( enclosingNode && enclosingNode.type === "Program" && enclosingNode.body.length === 0 && @@ -727,6 +726,7 @@ function handleOnlyComments({ comment, enclosingNode, ast, isLastComment }) { } return true; } + return false; } @@ -886,22 +886,6 @@ function isRealFunctionLikeNode(node) { ); } -/** - * @param {Node} enclosingNode - * @returns {RegExp | void} - */ -function getGapRegex(enclosingNode) { - if ( - enclosingNode && - enclosingNode.type !== "BinaryExpression" && - enclosingNode.type !== "LogicalExpression" - ) { - // Support degenerate single-element unions and intersections. - // E.g.: `type A = /* 1 */ & B` - return /^[\s&(|]*$/; - } -} - /** * @param {any} node * @returns {Node[] | void} @@ -947,7 +931,7 @@ function isTypeCastComment(comment) { } /** - * @param {FastPath} path + * @param {AstPath} path * @returns {boolean} */ function willPrintOwnComments(path /*, options */) { @@ -958,9 +942,7 @@ function willPrintOwnComments(path /*, options */) { ((node && (isJsxNode(node) || hasFlowShorthandAnnotationComment(node) || - (parent && - (parent.type === "CallExpression" || - parent.type === "OptionalCallExpression") && + (isCallExpression(parent) && (hasFlowAnnotationComment(node.leadingComments) || hasFlowAnnotationComment(node.trailingComments))))) || (parent && @@ -982,7 +964,6 @@ module.exports = { handleEndOfLineComment, handleRemainingComment, isTypeCastComment, - getGapRegex, getCommentChildNodes, willPrintOwnComments, }; diff --git a/src/language-js/embed/html.js b/src/language-js/embed/html.js index cb2e41306..99f5d9bde 100644 --- a/src/language-js/embed/html.js +++ b/src/language-js/embed/html.js @@ -59,7 +59,7 @@ function format(path, print, textToDoc, options, { parser }) { if (i % 2 === 0) { if (component) { component = uncookTemplateElementValue(component); - if (options.embeddedInHtml) { + if (options.__embeddedInHtml) { component = component.replace(/<\/(script)\b/gi, "<\\/$1"); } parts.push(component); @@ -67,7 +67,7 @@ function format(path, print, textToDoc, options, { parser }) { continue; } - const placeholderIndex = +component; + const placeholderIndex = Number(component); parts.push(expressionDocs[placeholderIndex]); } diff --git a/src/language-js/index.js b/src/language-js/index.js index dd35ccd6c..1c22c7014 100644 --- a/src/language-js/index.js +++ b/src/language-js/index.js @@ -21,7 +21,7 @@ const languages = [ ], vscodeLanguageIds: ["javascript", "mongo"], extensions: [ - ...data.extensions, + ...data.extensions.filter((extension) => extension !== ".jsx"), // WeiXin Script (Weixin Mini Programs) // https://developers.weixin.qq.com/miniprogram/en/dev/framework/view/wxs/ ".wxs", @@ -37,7 +37,8 @@ const languages = [ filenames: [], extensions: [".js.flow"], })), - createLanguage(require("linguist-languages/data/JSX.json"), () => ({ + createLanguage(require("linguist-languages/data/JavaScript.json"), () => ({ + name: "JSX", since: "0.0.0", parsers: [ "babel", @@ -49,6 +50,16 @@ const languages = [ "meriyah", ], vscodeLanguageIds: ["javascriptreact"], + aliases: undefined, + filenames: undefined, + extensions: [".jsx"], + group: "JavaScript", + interpreters: undefined, + tmScope: "source.js.jsx", + aceMode: "javascript", + codemirrorMode: "jsx", + codemirrorMimeType: "text/jsx", + color: undefined, })), createLanguage(require("linguist-languages/data/TypeScript.json"), () => ({ since: "1.4.0", diff --git a/src/language-js/loc.js b/src/language-js/loc.js index 68b020010..080650428 100644 --- a/src/language-js/loc.js +++ b/src/language-js/loc.js @@ -23,8 +23,7 @@ function locStart(node, opts) { } function locEnd(node) { - const end = node.range ? node.range[1] : node.end; - return node.typeAnnotation ? Math.max(end, locEnd(node.typeAnnotation)) : end; + return node.range ? node.range[1] : node.end; } /** diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index fccf92ca4..f897421e5 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -10,6 +10,8 @@ const { startsWithNoLookaheadToken, shouldFlatten, getPrecedence, + isCallExpression, + isMemberExpression, } = require("./utils"); function needsParens(path, options) { @@ -85,7 +87,8 @@ function needsParens(path, options) { node.type === "TaggedTemplateExpression" || node.type === "UnaryExpression" || node.type === "UpdateExpression" || - node.type === "YieldExpression") + node.type === "YieldExpression" || + node.type === "TSNonNullExpression") ) { return true; } @@ -402,8 +405,10 @@ function needsParens(path, options) { case "TSUnionType": case "TSIntersectionType": if ( - parent.type === "TSUnionType" || - parent.type === "TSIntersectionType" + (parent.type === "TSUnionType" || + parent.type === "TSIntersectionType") && + parent.types.length > 1 && + (!node.types || node.types.length > 1) ) { return true; } @@ -492,32 +497,46 @@ function needsParens(path, options) { if (name === "body" && parent.type === "ArrowFunctionExpression") { return true; - } else if ( + } + + if ( name === "key" && (parent.type === "ClassProperty" || parent.type === "PropertyDefinition") && parent.computed ) { return false; - } else if ( + } + + if ( (name === "init" || name === "update") && parent.type === "ForStatement" ) { return false; - } else if (parent.type === "ExpressionStatement") { + } + + if (parent.type === "ExpressionStatement") { return node.left.type === "ObjectPattern"; - } else if (name === "key" && parent.type === "TSPropertySignature") { + } + + if (name === "key" && parent.type === "TSPropertySignature") { return false; - } else if (parent.type === "AssignmentExpression") { + } + + if (parent.type === "AssignmentExpression") { return false; - } else if ( + } + + if ( parent.type === "SequenceExpression" && grandParent && grandParent.type === "ForStatement" && (grandParent.init === parent || grandParent.update === parent) ) { return false; - } else if ( + } + + if ( name === "value" && parent.type === "Property" && grandParent && @@ -525,9 +544,12 @@ function needsParens(path, options) { grandParent.properties.includes(parent) ) { return false; - } else if (parent.type === "NGChainedExpression") { + } + + if (parent.type === "NGChainedExpression") { return false; } + return true; } case "ConditionalExpression": @@ -581,7 +603,7 @@ function needsParens(path, options) { case "ArrowFunctionExpression": switch (parent.type) { case "PipelineTopicExpression": - return !!(node.extra && node.extra.parenthesized); + return Boolean(node.extra && node.extra.parenthesized); case "BinaryExpression": return ( @@ -597,6 +619,7 @@ function needsParens(path, options) { return name === "object"; case "TSAsExpression": + case "TSNonNullExpression": case "BindExpression": case "TaggedTemplateExpression": case "UnaryExpression": @@ -675,9 +698,7 @@ function needsParens(path, options) { (name === "callee" && (parent.type === "BindExpression" || parent.type === "NewExpression")) || - (name === "object" && - (parent.type === "MemberExpression" || - parent.type === "OptionalMemberExpression")) + (name === "object" && isMemberExpression(parent)) ); case "NGPipeExpression": if ( @@ -687,9 +708,7 @@ function needsParens(path, options) { // Preserve parens for compatibility with AngularJS expressions !(node.extra && node.extra.parenthesized)) || parent.type === "ArrayExpression" || - ((parent.type === "CallExpression" || - parent.type === "OptionalCallExpression") && - parent.arguments[name] === node) || + (isCallExpression(parent) && parent.arguments[name] === node) || (name === "right" && parent.type === "NGPipeExpression") || (name === "property" && parent.type === "MemberExpression") || parent.type === "AssignmentExpression" @@ -709,7 +728,6 @@ function needsParens(path, options) { parent.type !== "AssignmentExpression" && parent.type !== "AssignmentPattern" && parent.type !== "BinaryExpression" && - parent.type !== "CallExpression" && parent.type !== "NewExpression" && parent.type !== "ConditionalExpression" && parent.type !== "ExpressionStatement" && @@ -720,7 +738,7 @@ function needsParens(path, options) { parent.type !== "JSXFragment" && parent.type !== "LogicalExpression" && parent.type !== "ObjectProperty" && - parent.type !== "OptionalCallExpression" && + !isCallExpression(parent) && parent.type !== "Property" && parent.type !== "ReturnStatement" && parent.type !== "ThrowStatement" && diff --git a/src/language-js/parse-postprocess.js b/src/language-js/parse-postprocess.js index be9dcd9dd..2a9b4fbae 100644 --- a/src/language-js/parse-postprocess.js +++ b/src/language-js/parse-postprocess.js @@ -129,15 +129,6 @@ function postprocess(ast, options) { node.typeAnnotation.range = [locStart(node), locEnd(node)]; return node.typeAnnotation; } - case "TSUnionType": - case "TSIntersectionType": - if (node.types.length === 1) { - const [firstType] = node.types; - // override loc, so that comments are attached properly - firstType.range = [locStart(node), locEnd(node)]; - return firstType; - } - break; case "TSTypeParameter": // babel-ts if (typeof node.name === "string") { diff --git a/src/language-js/parser-babel.js b/src/language-js/parser-babel.js index dad0eded4..3e4bad413 100644 --- a/src/language-js/parser-babel.js +++ b/src/language-js/parser-babel.js @@ -1,7 +1,6 @@ "use strict"; const flatten = require("lodash/flatten"); -const createError = require("../common/parser-create-error"); const tryCombinations = require("../utils/try-combinations"); const { getNextNonSpaceNonCommentCharacterIndexWithStartIndex, @@ -9,6 +8,8 @@ const { } = require("../common/util"); const postprocess = require("./parse-postprocess"); const createParser = require("./parser/create-parser"); +const createBabelParseError = require("./parser/create-babel-parse-error"); +const jsonParsers = require("./parser/json"); const parseOptions = { sourceType: "module", @@ -48,6 +49,10 @@ const pipelineOperatorPlugins = [ ["pipelineOperator", { proposal: "minimal" }], ["pipelineOperator", { proposal: "fsharp" }], ]; +const appendPlugins = (plugins) => ({ + ...parseOptions, + plugins: [...parseOptions.plugins, ...plugins], +}); // Similar to babel // https://github.com/babel/babel/pull/7934/files#diff-a739835084910b0ee3ea649df5a4d223R67 @@ -87,72 +92,57 @@ function parseWithOptions(parseMethod, text, options) { return ast; } -function createParseError(error) { - // babel error prints (l:c) with cols that are zero indexed - // so we need our custom error - const { message, loc } = error; - - return createError(message.replace(/ \(.*\)/, ""), { - start: { - line: loc ? loc.line : 0, - column: loc ? loc.column + 1 : 0, - }, - }); -} - -function createParse(parseMethod, ...pluginCombinations) { - const commonPlugins = parseOptions.plugins; - pluginCombinations = - pluginCombinations.length > 0 - ? pluginCombinations.map((plugins) => [...commonPlugins, ...plugins]) - : [commonPlugins]; - +function createParse(parseMethod, ...optionsCombinations) { return (text, parsers, opts = {}) => { if (opts.parser === "babel" && isFlowFile(text, opts)) { opts.parser = "babel-flow"; return parseFlow(text, parsers, opts); } - let combinations = pluginCombinations; + let combinations = optionsCombinations; + if (opts.__babelSourceType === "script") { + combinations = combinations.map((options) => ({ + ...options, + sourceType: "script", + })); + } + if (text.includes("|>")) { combinations = flatten( pipelineOperatorPlugins.map((pipelineOperatorPlugin) => - combinations.map((plugins) => [...plugins, pipelineOperatorPlugin]) + combinations.map((options) => ({ + ...options, + plugins: [...options.plugins, pipelineOperatorPlugin], + })) ) ); } - const sourceType = - opts.__babelSourceType === "script" ? "script" : "module"; const { result: ast, error } = tryCombinations( - ...combinations.map((plugins) => () => - parseWithOptions(parseMethod, text, { - ...parseOptions, - sourceType, - plugins, - }) + ...combinations.map((options) => () => + parseWithOptions(parseMethod, text, options) ) ); if (!ast) { - throw createParseError(error); + throw createBabelParseError(error); } return postprocess(ast, { ...opts, originalText: text }); }; } -const parse = createParse("parse", ["jsx", "flow"]); -const parseFlow = createParse("parse", [ - "jsx", - ["flow", { all: true, enums: true }], -]); +const parse = createParse("parse", appendPlugins(["jsx", "flow"])); +const parseFlow = createParse( + "parse", + appendPlugins(["jsx", ["flow", { all: true, enums: true }]]) +); const parseTypeScript = createParse( "parse", - ["jsx", "typescript"], - ["typescript"] + appendPlugins(["jsx", "typescript"]), + appendPlugins(["typescript"]) ); -const parseExpression = createParse("parseExpression", ["jsx"]); +const parseExpression = createParse("parseExpression", appendPlugins(["jsx"])); const messagesShouldThrow = new Set([ // TSErrors.UnexpectedTypeAnnotation @@ -167,6 +157,12 @@ const messagesShouldThrow = new Set([ // Rethrow on omitted call arguments: foo("a", , "b"); // ErrorMessages.UnexpectedToken "Unexpected token ','", + // ErrorMessages.EscapedCharNotAnIdentifier + "Invalid Unicode escape", + // ErrorMessages.MissingUnicodeEscape + "Expecting Unicode escape sequence \\uXXXX", + // ErrorMessages.MissingSemicolon + "Missing semicolon", ]); function shouldRethrowRecoveredError(error) { @@ -175,80 +171,6 @@ function shouldRethrowRecoveredError(error) { return messagesShouldThrow.has(message); } -function parseJson(text, parsers, opts) { - const ast = parseExpression(text, parsers, opts); - - for (const comment of ast.comments) { - assertJsonNode(comment); - } - assertJsonNode(ast); - - return ast; -} - -function assertJsonNode(node, parent) { - switch (node.type) { - case "ArrayExpression": - for (const element of node.elements) { - assertJsonChildNode(element); - } - return; - case "ObjectExpression": - for (const property of node.properties) { - assertJsonChildNode(property); - } - return; - case "ObjectProperty": - if (node.computed) { - throw createJsonError("computed"); - } - - if (node.shorthand) { - throw createJsonError("shorthand"); - } - - assertJsonChildNode(node.key); - assertJsonChildNode(node.value); - return; - case "UnaryExpression": - switch (node.operator) { - case "+": - case "-": - return assertJsonChildNode(node.argument); - default: - throw createJsonError("operator"); - } - case "Identifier": - if (parent && parent.type === "ObjectProperty" && parent.key === node) { - return; - } - throw createJsonError(); - case "NullLiteral": - case "BooleanLiteral": - case "NumericLiteral": - case "StringLiteral": - return; - default: - throw createJsonError(); - } - - function assertJsonChildNode(child) { - return assertJsonNode(child, node); - } - - function createJsonError(attribute) { - const name = !attribute - ? node.type - : `${node.type} with ${attribute}=${JSON.stringify(node[attribute])}`; - return createError(`${name} is not allowed in JSON.`, { - start: { - line: node.loc.start.line, - column: node.loc.start.column + 1, - }, - }); - } -} - const babel = createParser(parse); const babelExpression = createParser(parseExpression); @@ -258,17 +180,7 @@ module.exports = { babel, "babel-flow": createParser(parseFlow), "babel-ts": createParser(parseTypeScript), - json: { - ...babelExpression, - hasPragma() { - return true; - }, - }, - json5: babelExpression, - "json-stringify": createParser({ - parse: parseJson, - astFormat: "estree-json", - }), + ...jsonParsers, /** @internal */ __js_expression: babelExpression, /** for vue filter */ diff --git a/src/language-js/parser-flow.js b/src/language-js/parser-flow.js index 901f1b52e..91662089e 100644 --- a/src/language-js/parser-flow.js +++ b/src/language-js/parser-flow.js @@ -4,14 +4,31 @@ const createError = require("../common/parser-create-error"); const postprocess = require("./parse-postprocess"); const createParser = require("./parser/create-parser"); +// https://github.com/facebook/flow/tree/master/packages/flow-parser#options const parseOptions = { + // `all_comments` (boolean, default `true`) - include a list of all comments from the whole program + // all_comments: true, + // `comments` (boolean, default `true`) - attach comments to AST nodes (`leadingComments` and `trailingComments`) + comments: false, + // `enums` (boolean, default `false`) - enable parsing of Flow enums enums: true, - esproposal_decorators: true, + // `esproposal_class_instance_fields` (boolean, default `false`) - enable parsing of class instance fields esproposal_class_instance_fields: true, + // `esproposal_class_static_fields` (boolean, default `false`) - enable parsing of class static fields esproposal_class_static_fields: true, + // `esproposal_decorators` (boolean, default `false`) - enable parsing of decorators + esproposal_decorators: true, + // `esproposal_export_star_as` (boolean, default `false`) - enable parsing of `export * as` syntax esproposal_export_star_as: true, - esproposal_optional_chaining: true, + // `esproposal_nullish_coalescing` (boolean, default `false`) - enable parsing of nullish coalescing (`??`) esproposal_nullish_coalescing: true, + // `esproposal_optional_chaining` (boolean, default `false`) - enable parsing of optional chaining (`?.`) + esproposal_optional_chaining: true, + // `types` (boolean, default `true`) - enable parsing of Flow types + // types: true, + // `use_strict` (boolean, default `false`) - treat the file as strict, without needing a "use strict" directive + // use_strict: false, + // Not documented tokens: true, }; diff --git a/src/language-js/parser/create-babel-parse-error.js b/src/language-js/parser/create-babel-parse-error.js new file mode 100644 index 000000000..7d14c0634 --- /dev/null +++ b/src/language-js/parser/create-babel-parse-error.js @@ -0,0 +1,18 @@ +"use strict"; + +const createError = require("../../common/parser-create-error"); + +function createBabelParseError(error) { + // babel error prints (l:c) with cols that are zero indexed + // so we need our custom error + const { message, loc } = error; + + return createError(message.replace(/ \(.*\)/, ""), { + start: { + line: loc ? loc.line : 0, + column: loc ? loc.column + 1 : 0, + }, + }); +} + +module.exports = createBabelParseError; diff --git a/src/language-js/parser/json.js b/src/language-js/parser/json.js new file mode 100644 index 000000000..7675a3232 --- /dev/null +++ b/src/language-js/parser/json.js @@ -0,0 +1,136 @@ +"use strict"; + +const { isNonEmptyArray } = require("../../common/util"); +const createError = require("../../common/parser-create-error"); +const createParser = require("./create-parser"); +const createBabelParseError = require("./create-babel-parse-error"); + +function createJsonParse(options = {}) { + const { allowComments = true } = options; + + return function parse(text /*, parsers, options*/) { + let ast; + try { + ast = require("@babel/parser").parseExpression(text, { + tokens: true, + ranges: true, + }); + } catch (error) { + throw createBabelParseError(error); + } + + // @ts-ignore + if (!allowComments && isNonEmptyArray(ast.comments)) { + // @ts-ignore + throw createJsonError(ast.comments[0], "Comment"); + } + + assertJsonNode(ast); + + return ast; + }; +} + +function createJsonError(node, description) { + const [start, end] = [node.loc.start, node.loc.end].map( + ({ line, column }) => ({ + line, + column: column + 1, + }) + ); + return createError(`${description} is not allowed in JSON.`, { start, end }); +} + +function assertJsonNode(node) { + switch (node.type) { + case "ArrayExpression": + for (const element of node.elements) { + if (element !== null) { + assertJsonNode(element); + } + } + + return; + case "ObjectExpression": + for (const property of node.properties) { + assertJsonNode(property); + } + + return; + case "ObjectProperty": + if (node.computed) { + throw createJsonError(node.key, "Computed key"); + } + + if (node.shorthand) { + throw createJsonError(node.key, "Shorthand property"); + } + + if (node.key.type !== "Identifier") { + assertJsonNode(node.key); + } + + assertJsonNode(node.value); + + return; + case "UnaryExpression": + if (node.operator !== "+" && node.operator !== "-") { + throw createJsonError(node, `Operator '${node.operator}'`); + } + + assertJsonNode(node.argument); + + return; + case "Identifier": + if ( + // JSON5 https://spec.json5.org/#numbers + node.name !== "Infinity" && + node.name !== "NaN" && + // JSON6 https://github.com/d3x0r/JSON6 + node.name !== "undefined" + ) { + throw createJsonError(node, `Identifier '${node.name}'`); + } + + return; + case "TemplateLiteral": + if (isNonEmptyArray(node.expressions)) { + throw createJsonError( + node.expressions[0], + "'TemplateLiteral' with expression" + ); + } + + for (const element of node.quasis) { + assertJsonNode(element); + } + + return; + case "NullLiteral": + case "BooleanLiteral": + case "NumericLiteral": + case "StringLiteral": + case "TemplateElement": + return; + default: + throw createJsonError(node, `'${node.type}'`); + } +} + +const parseJson = createJsonParse(); + +const jsonParsers = { + json: createParser({ + parse: parseJson, + hasPragma() { + return true; + }, + }), + json5: createParser(parseJson), + "json-stringify": createParser({ + parse: createJsonParse({ allowComments: false }), + astFormat: "estree-json", + }), +}; + +module.exports = jsonParsers; diff --git a/src/language-js/print/angular.js b/src/language-js/print/angular.js index 166150a0f..33bec62c8 100644 --- a/src/language-js/print/angular.js +++ b/src/language-js/print/angular.js @@ -6,18 +6,18 @@ const { const { hasNode, hasComment, getComments } = require("../utils"); const { printBinaryishExpression } = require("./binaryish"); -/** @typedef {import("../../common/fast-path")} FastPath */ +/** @typedef {import("../../common/ast-path")} AstPath */ function printAngular(path, options, print) { const n = path.getValue(); switch (n.type) { case "NGRoot": - return [].concat( + return [ path.call(print, "node"), !hasComment(n.node) - ? [] - : [" //", getComments(n.node)[0].value.trimEnd()] - ); + ? "" + : " //" + getComments(n.node)[0].value.trimEnd(), + ]; case "NGPipeExpression": return printBinaryishExpression(path, options, print); case "NGChainedExpression": @@ -99,7 +99,7 @@ function isNgForOf(node, index, parentNode) { /** identify if an angular expression seems to have side effects */ /** - * @param {FastPath} path + * @param {AstPath} path * @returns {boolean} */ function hasNgSideEffect(path) { diff --git a/src/language-js/print/array.js b/src/language-js/print/array.js index d8ad213da..afa53aafb 100644 --- a/src/language-js/print/array.js +++ b/src/language-js/print/array.js @@ -2,15 +2,18 @@ const { printDanglingComments } = require("../../main/comments"); const { - builders: { line, softline, group, indent, ifBreak }, + builders: { line, softline, hardline, group, indent, ifBreak, fill }, } = require("../../document"); -const { getLast } = require("../../common/util"); +const { getLast, hasNewline } = require("../../common/util"); const { shouldPrintComma, hasComment, CommentCheckFlags, isNextLineEmpty, + isNumericLiteral, + isSignedNumericLiteral, } = require("../utils"); +const { locStart } = require("../loc"); const { printOptionalToken, printTypeAnnotation } = require("./misc"); @@ -50,7 +53,9 @@ function printArray(path, options, print) { // // Note that getLast returns null if the array is empty, but // we already check for an empty array just above so we are safe - const needsForcedTrailingComma = canHaveTrailingComma && lastElem === null; + const needsForcedTrailingComma = lastElem === null; + + const groupId = Symbol("array"); const shouldBreak = !options.__inJestEach && @@ -75,24 +80,52 @@ function printArray(path, options, print) { return element[itemsKey] && element[itemsKey].length > 1; }); + const shouldUseConciseFormatting = + n.elements.length > 1 && + n.elements.every( + (element) => + element && + (isNumericLiteral(element) || + (isSignedNumericLiteral(element) && + !hasComment(element.argument))) && + !hasComment( + element, + CommentCheckFlags.Trailing | CommentCheckFlags.Line, + (comment) => + !hasNewline(options.originalText, locStart(comment), { + backwards: true, + }) + ) + ); + + const trailingComma = !canHaveTrailingComma + ? "" + : needsForcedTrailingComma + ? "," + : !shouldPrintComma(options) + ? "" + : shouldUseConciseFormatting + ? ifBreak(",", "", { groupId }) + : ifBreak(","); + parts.push( group( [ openBracket, - indent([softline, printArrayItems(path, options, "elements", print)]), - needsForcedTrailingComma ? "," : "", - ifBreak( - canHaveTrailingComma && - !needsForcedTrailingComma && - shouldPrintComma(options) - ? "," - : "" - ), - printDanglingComments(path, options, /* sameIndent */ true), + indent([ + softline, + shouldUseConciseFormatting + ? printArrayItemsConcisely(path, options, print, trailingComma) + : [ + printArrayItems(path, options, "elements", print), + trailingComma, + ], + printDanglingComments(path, options, /* sameIndent */ true), + ]), softline, closeBracket, ], - { shouldBreak } + { shouldBreak, id: groupId } ) ); } @@ -124,4 +157,29 @@ function printArrayItems(path, options, printPath, print) { return printedElements; } +function printArrayItemsConcisely(path, options, print, trailingComma) { + const parts = []; + + path.each((childPath, i, elements) => { + const isLast = i === elements.length - 1; + + parts.push([print(childPath), isLast ? trailingComma : ","]); + + if (!isLast) { + parts.push( + isNextLineEmpty(childPath.getValue(), options) + ? [hardline, hardline] + : hasComment( + elements[i + 1], + CommentCheckFlags.Leading | CommentCheckFlags.Line + ) + ? hardline + : line + ); + } + }, "elements"); + + return fill(parts); +} + module.exports = { printArray, printArrayItems }; diff --git a/src/language-js/print/assignment.js b/src/language-js/print/assignment.js index 1a90748f1..ba24f2ed1 100644 --- a/src/language-js/print/assignment.js +++ b/src/language-js/print/assignment.js @@ -1,95 +1,260 @@ "use strict"; -const { isNonEmptyArray } = require("../../common/util"); +const { isNonEmptyArray, getStringWidth } = require("../../common/util"); const { - builders: { line, group, indent }, + builders: { line, group, indent, indentIfBreak }, + utils: { cleanDoc }, } = require("../../document"); const { hasLeadingOwnLineComment, isBinaryish, - isMemberExpressionChain, isStringLiteral, + isNumericLiteral, + isCallExpression, + isMemberExpression, + getCallArguments, + isSimpleCallArgument, } = require("../utils"); const { shouldInlineLogicalExpression } = require("./binaryish"); function printAssignment( - leftNode, - printedLeft, + path, + options, + print, + leftDoc, operator, - rightNode, - printedRight, - options + rightPropertyName ) { - if (!rightNode) { - return printedLeft; + const rightDoc = path.call(print, rightPropertyName); + + switch (chooseLayout(path, options, leftDoc, rightPropertyName)) { + // First break after operator, then the sides are broken independently on their own lines + case "break-after-operator": + return group([group(leftDoc), operator, group(indent([line, rightDoc]))]); + + // First break right-hand side, then left-hand side + case "never-break-after-operator": + return group([group(leftDoc), operator, " ", rightDoc]); + + // First break right-hand side, then after operator + case "fluid": { + const groupId = Symbol("assignment"); + return group([ + group(leftDoc), + operator, + group(indent(line), { id: groupId }), + indentIfBreak(rightDoc, { groupId }), + ]); + } + + // Parts of assignment chains aren't wrapped in groups. + // Once one of them breaks, the chain breaks too. + case "chain": + return [group(leftDoc), operator, line, rightDoc]; + + case "chain-tail": + return [group(leftDoc), operator, indent([line, rightDoc])]; + + case "only-left": + return leftDoc; } - - const printed = printAssignmentRight( - leftNode, - rightNode, - printedRight, - options - ); - - return group([printedLeft, operator, printed]); } function printAssignmentExpression(path, options, print) { const n = path.getValue(); return printAssignment( - n.left, + path, + options, + print, path.call(print, "left"), [" ", n.operator], - n.right, - path.call(print, "right"), - options + "right" ); } function printVariableDeclarator(path, options, print) { - const n = path.getValue(); return printAssignment( - n.id, + path, + options, + print, path.call(print, "id"), " =", - n.init, - n.init && path.call(print, "init"), - options + "init" ); } -function printAssignmentRight(leftNode, rightNode, printedRight, options) { - if (hasLeadingOwnLineComment(options.originalText, rightNode)) { - return indent([line, printedRight]); +function chooseLayout(path, options, leftDoc, rightPropertyName) { + const node = path.getValue(); + const rightNode = node[rightPropertyName]; + + if (!rightNode) { + return "only-left"; } - const canBreak = - (isBinaryish(rightNode) && !shouldInlineLogicalExpression(rightNode)) || - (rightNode.type === "ConditionalExpression" && - isBinaryish(rightNode.test) && - !shouldInlineLogicalExpression(rightNode.test)) || - rightNode.type === "StringLiteralTypeAnnotation" || - (rightNode.type === "ClassExpression" && - isNonEmptyArray(rightNode.decorators)) || - ((leftNode.type === "Identifier" || - isStringLiteral(leftNode) || - leftNode.type === "MemberExpression") && - (isStringLiteral(rightNode) || isMemberExpressionChain(rightNode)) && - // do not put values on a separate line from the key in json - options.parser !== "json" && - options.parser !== "json5") || - rightNode.type === "SequenceExpression"; + // Short assignment chains (only 2 segments) are NOT formatted as chains. + // 1) a = b = c; (expression statements) + // 2) var/let/const a = b = c; - if (canBreak) { - return group(indent([line, printedRight])); + const isTail = !isAssignment(rightNode); + const shouldUseChainFormatting = path.match( + isAssignment, + (node) => isAssignment(node) || node.type === "VariableDeclarator", + (node) => + !isTail || + (node.type !== "ExpressionStatement" && + node.type !== "VariableDeclaration") + ); + if (shouldUseChainFormatting) { + return isTail ? "chain-tail" : "chain"; + } + const isHeadOfLongChain = !isTail && isAssignment(rightNode.right); + + if ( + isHeadOfLongChain || + hasLeadingOwnLineComment(options.originalText, rightNode) + ) { + return "break-after-operator"; } - return [" ", printedRight]; + // do not put values on a separate line from the key in json + if (options.parser === "json5" || options.parser === "json") { + return "never-break-after-operator"; + } + + // wrapping object properties with very short keys usually doesn't add much value + const hasShortKey = isObjectPropertyWithShortKey(node, leftDoc, options); + + if (shouldBreakAfterOperator(rightNode, hasShortKey)) { + return "break-after-operator"; + } + + if (hasShortKey || shouldNeverBreakAfterOperator(rightNode)) { + return "never-break-after-operator"; + } + + return "fluid"; +} + +function shouldBreakAfterOperator(rightNode, hasShortKey) { + if (isBinaryish(rightNode) && !shouldInlineLogicalExpression(rightNode)) { + return true; + } + + switch (rightNode.type) { + case "StringLiteralTypeAnnotation": + case "SequenceExpression": + return true; + case "ConditionalExpression": { + const { test } = rightNode; + return isBinaryish(test) && !shouldInlineLogicalExpression(test); + } + case "ClassExpression": + return isNonEmptyArray(rightNode.decorators); + } + + if (hasShortKey) { + return false; + } + + let node = rightNode; + for (;;) { + if (node.type === "UnaryExpression") { + node = node.argument; + } else if (node.type === "TSNonNullExpression") { + node = node.expression; + } else { + break; + } + } + if ( + isStringLiteral(node) || + isMemberExpressionChainWithSimpleCalls(node) || + (isSimpleCall(node) && + isSimpleCall(node.callee) && + (isMemberExpressionChainHead(node.callee.callee) || + isMemberExpressionChainWithSimpleCalls(node.callee.callee))) + ) { + return true; + } + + return false; +} + +function shouldNeverBreakAfterOperator(rightNode) { + return ( + rightNode.type === "TemplateLiteral" || + rightNode.type === "TaggedTemplateExpression" || + rightNode.type === "BooleanLiteral" || + isNumericLiteral(rightNode) || + (rightNode.type === "CallExpression" && + rightNode.callee.name === "require") || + rightNode.type === "ClassExpression" + ); +} + +function isAssignment(node) { + return node.type === "AssignmentExpression"; +} + +function isMemberExpressionChainHead(node) { + return node.type === "Identifier" || node.type === "ThisExpression"; +} + +function isMemberExpressionChainWithSimpleCalls(node) { + if (!isMemberExpression(node)) { + return false; + } + let { object } = node; + for (;;) { + if (object.type === "TSNonNullExpression") { + object = object.expression; + } else if (isCallExpression(object)) { + if (!isSimpleCall(object)) { + return false; + } + object = object.callee; + } else { + break; + } + } + return ( + isMemberExpressionChainHead(object) || + isMemberExpressionChainWithSimpleCalls(object) + ); +} + +function isSimpleCall(node) { + if (!isCallExpression(node)) { + return false; + } + const args = getCallArguments(node); + return ( + args.length === 0 || (args.length === 1 && isSimpleCallArgument(args[0], 1)) + ); +} + +function isObjectPropertyWithShortKey(node, keyDoc, options) { + if (node.type !== "ObjectProperty" && node.type !== "Property") { + return false; + } + // TODO: for performance, it might make sense to use a more lightweight + // version of cleanDoc, such that it would stop once it detects that + // the doc can't be reduced to a string. + keyDoc = cleanDoc(keyDoc); + const MIN_OVERLAP_FOR_BREAK = 3; + // ↓↓ - insufficient overlap for a line break + // key1: longValue1, + // ↓↓↓↓↓↓ - overlap is long enough to break + // key2abcd: + // longValue2 + return ( + typeof keyDoc === "string" && + getStringWidth(keyDoc) < options.tabWidth + MIN_OVERLAP_FOR_BREAK + ); } module.exports = { printVariableDeclarator, printAssignmentExpression, printAssignment, - printAssignmentRight, }; diff --git a/src/language-js/print/binaryish.js b/src/language-js/print/binaryish.js index c38235cbd..150ddb4c9 100644 --- a/src/language-js/print/binaryish.js +++ b/src/language-js/print/binaryish.js @@ -3,8 +3,17 @@ const { printComments } = require("../../main/comments"); const { getLast } = require("../../common/util"); const { - builders: { join, line, softline, group, indent, align, ifBreak }, - utils: { cleanDoc, getDocParts }, + builders: { + join, + line, + softline, + group, + indent, + align, + ifBreak, + indentIfBreak, + }, + utils: { cleanDoc, getDocParts, isConcat }, } = require("../../document"); const { hasLeadingOwnLineComment, @@ -13,6 +22,8 @@ const { shouldFlatten, hasComment, CommentCheckFlags, + isCallExpression, + isMemberExpression, } = require("../utils"); /** @typedef {import("../../document").Doc} Doc */ @@ -60,13 +71,9 @@ function printBinaryishExpression(path, options, print) { // c // ).call() if ( - ((parent.type === "CallExpression" || - parent.type === "OptionalCallExpression") && - parent.callee === n) || + (isCallExpression(parent) && parent.callee === n) || parent.type === "UnaryExpression" || - ((parent.type === "MemberExpression" || - parent.type === "OptionalMemberExpression") && - !parent.computed) + (isMemberExpression(parent) && !parent.computed) ) { return group([indent([softline, ...parts]), softline]); } @@ -89,8 +96,7 @@ function printBinaryishExpression(path, options, print) { (parent.type === "ConditionalExpression" && parentParent.type !== "ReturnStatement" && parentParent.type !== "ThrowStatement" && - parentParent.type !== "CallExpression" && - parentParent.type !== "OptionalCallExpression") || + !isCallExpression(parentParent)) || parent.type === "TemplateLiteral"; const shouldIndentIfInlining = @@ -160,7 +166,7 @@ function printBinaryishExpression(path, options, print) { } const jsxPart = getLast(parts); - return group([chain, ifBreak(indent(jsxPart), jsxPart, { groupId })]); + return group([chain, indentIfBreak(jsxPart, { groupId })]); } // For binary expressions to be consistent, we need to group @@ -196,8 +202,9 @@ function printBinaryishExpressions( // which is unique in that it is right-associative.) if (shouldFlatten(node.operator, node.left.operator)) { // Flatten them out by recursively calling this function. - parts = parts.concat( - path.call( + parts = [ + ...parts, + ...path.call( (left) => printBinaryishExpressions( left, @@ -207,8 +214,8 @@ function printBinaryishExpressions( isInsideParenthesis ), "left" - ) - ); + ), + ]; } else { parts.push(group(path.call(print, "left"))); } @@ -268,12 +275,12 @@ function printBinaryishExpressions( // the other ones since we don't call the normal print on BinaryExpression, // only for the left and right parts if (isNested && hasComment(node)) { - const printed = cleanDoc(printComments(path, () => parts, options)); - /* istanbul ignore if */ - if (printed.type === "string") { - parts = [printed]; - } else { + const printed = cleanDoc(printComments(path, parts, options)); + /* istanbul ignore else */ + if (isConcat(printed) || printed.type === "fill") { parts = getDocParts(printed); + } else { + parts = [printed]; } } } else { diff --git a/src/language-js/print/call-arguments.js b/src/language-js/print/call-arguments.js index 9ee644498..e662860ea 100644 --- a/src/language-js/print/call-arguments.js +++ b/src/language-js/print/call-arguments.js @@ -14,6 +14,7 @@ const { getCallArguments, iterateCallArgumentsPath, isNextLineEmpty, + isCallExpression, } = require("../utils"); const { @@ -44,14 +45,7 @@ function printCallArguments(path, options, print) { } // useEffect(() => { ... }, [foo, bar, baz]) - if ( - args.length === 2 && - args[0].type === "ArrowFunctionExpression" && - getFunctionParameters(args[0]).length === 0 && - args[0].body.type === "BlockStatement" && - args[1].type === "ArrayExpression" && - !args.some((arg) => hasComment(arg)) - ) { + if (isReactHookCallWithDepsArray(args)) { return [ "(", path.call(print, "arguments", 0), @@ -159,12 +153,14 @@ function printCallArguments(path, options, print) { hasEmptyLineFollowingFirstArg ? hardline : line, hasEmptyLineFollowingFirstArg ? hardline : "", ], - ].concat(printedArguments.slice(1)); + ...printedArguments.slice(1), + ]; } if (shouldGroupLast && i === args.length - 1) { - printedExpanded = printedArguments - .slice(0, -1) - .concat(argPath.call((p) => print(p, { expandLastArg: true }))); + printedExpanded = [ + ...printedArguments.slice(0, -1), + argPath.call((p) => print(p, { expandLastArg: true })), + ]; } }); @@ -244,13 +240,14 @@ function couldGroupArg(arg) { // }); (!arg.returnType || !arg.returnType.typeAnnotation || - arg.returnType.typeAnnotation.type !== "TSTypeReference") && + arg.returnType.typeAnnotation.type !== "TSTypeReference" || + // https://github.com/prettier/prettier/issues/7542 + isNonEmptyBlockStatement(arg.body)) && (arg.body.type === "BlockStatement" || arg.body.type === "ArrowFunctionExpression" || arg.body.type === "ObjectExpression" || arg.body.type === "ArrayExpression" || - arg.body.type === "CallExpression" || - arg.body.type === "OptionalCallExpression" || + isCallExpression(arg.body) || arg.body.type === "ConditionalExpression" || isJsxNode(arg.body))) ); @@ -265,7 +262,11 @@ function shouldGroupLastArg(args) { couldGroupArg(lastArg) && // If the last two arguments are of the same type, // disable last element expansion. - (!penultimateArg || penultimateArg.type !== lastArg.type) + (!penultimateArg || penultimateArg.type !== lastArg.type) && + // useMemo(() => func(), [foo, bar, baz]) + (args.length !== 2 || + args[0].type !== "ArrowFunctionExpression" || + args[1].type !== "ArrayExpression") ); } @@ -287,4 +288,23 @@ function shouldGroupFirstArg(args) { ); } +function isReactHookCallWithDepsArray(args) { + return ( + args.length === 2 && + args[0].type === "ArrowFunctionExpression" && + getFunctionParameters(args[0]).length === 0 && + args[0].body.type === "BlockStatement" && + args[1].type === "ArrayExpression" && + !args.some((arg) => hasComment(arg)) + ); +} + +function isNonEmptyBlockStatement(node) { + return ( + node.type === "BlockStatement" && + (node.body.some((node) => node.type !== "EmptyStatement") || + hasComment(node, CommentCheckFlags.Dangling)) + ); +} + module.exports = printCallArguments; diff --git a/src/language-js/print/call-expression.js b/src/language-js/print/call-expression.js index 2b5a0941e..6b33179fc 100644 --- a/src/language-js/print/call-expression.js +++ b/src/language-js/print/call-expression.js @@ -7,7 +7,7 @@ const pathNeedsParens = require("../needs-parens"); const { getCallArguments, hasFlowAnnotationComment, - isCallOrOptionalCallExpression, + isCallExpression, isMemberish, isTemplateOnItsOwnLine, isTestCall, @@ -95,7 +95,7 @@ function printCallExpression(path, options, print) { // We group here when the callee is itself a call expression. // See `isLongCurriedCallExpression` for more info. - if (isDynamicImport || isCallOrOptionalCallExpression(n.callee)) { + if (isDynamicImport || isCallExpression(n.callee)) { return group(contents); } diff --git a/src/language-js/print/class.js b/src/language-js/print/class.js index 4f53b458c..df375f059 100644 --- a/src/language-js/print/class.js +++ b/src/language-js/print/class.js @@ -5,16 +5,13 @@ const { printComments, printDanglingComments } = require("../../main/comments"); const { builders: { join, line, hardline, softline, group, indent, ifBreak }, } = require("../../document"); -const { - hasComment, - CommentCheckFlags, - hasNewlineBetweenOrAfterDecorators, -} = require("../utils"); +const { hasComment, CommentCheckFlags } = require("../utils"); const { getTypeParametersGroupId } = require("./type-parameters"); const { printMethod } = require("./function"); const { printOptionalToken, printTypeAnnotation } = require("./misc"); const { printPropertyKey } = require("./property"); -const { printAssignmentRight } = require("./assignment"); +const { printAssignment } = require("./assignment"); +const { printClassMemberDecorators } = require("./decorators"); function printClass(path, options, print) { const n = path.getValue(); @@ -55,7 +52,7 @@ function printClass(path, options, print) { path.call(print, "superTypeParameters"), ]; const printedWithComments = path.call( - (superClass) => printComments(superClass, () => printed, options), + (superClass) => printComments(superClass, printed, options), "superClass" ); if (groupMode) { @@ -73,15 +70,11 @@ function printClass(path, options, print) { ); if (groupMode) { - const printedExtends = extendsParts; let printedPartsGroup; if (shouldIndentOnlyHeritageClauses(n)) { - printedPartsGroup = [ - ...partsGroup, - ifBreak(indent(printedExtends), printedExtends), - ]; + printedPartsGroup = [...partsGroup, indent(extendsParts)]; } else { - printedPartsGroup = indent([...partsGroup, printedExtends]); + printedPartsGroup = indent([...partsGroup, extendsParts]); } parts.push(group(printedPartsGroup, { id: getHeritageGroupId(n) })); } else { @@ -101,8 +94,8 @@ function printHardlineAfterHeritage(node) { function hasMultipleHeritage(node) { return ( - ["superClass", "extends", "mixins", "implements"].filter( - (key) => !!node[key] + ["superClass", "extends", "mixins", "implements"].filter((key) => + Boolean(node[key]) ).length > 1 ); } @@ -159,11 +152,20 @@ function printClassMethod(path, options, print) { const parts = []; if (isNonEmptyArray(n.decorators)) { - parts.push(printDecorators(path, options, print)); + parts.push(printClassMemberDecorators(path, options, print)); } if (n.accessibility) { parts.push(n.accessibility + " "); } + // "readonly" and "declare" are supported by only "babel-ts" + // https://github.com/prettier/prettier/issues/9760 + if (n.readonly) { + parts.push("readonly "); + } + if (n.declare) { + parts.push("declare "); + } + if (n.static) { parts.push("static "); } @@ -182,7 +184,7 @@ function printClassProperty(path, options, print) { const semi = options.semi ? ";" : ""; if (isNonEmptyArray(n.decorators)) { - parts.push(printDecorators(path, options, print)); + parts.push(printClassMemberDecorators(path, options, print)); } if (n.accessibility) { parts.push(n.accessibility + " "); @@ -207,24 +209,8 @@ function printClassProperty(path, options, print) { printOptionalToken(path), printTypeAnnotation(path, options, print) ); - if (n.value) { - parts.push( - " =", - printAssignmentRight(n.key, n.value, path.call(print, "value"), options) - ); - } - parts.push(semi); - - return group(parts); -} - -function printDecorators(path, options, print) { - const node = path.getValue(); - return group([ - join(line, path.map(print, "decorators")), - hasNewlineBetweenOrAfterDecorators(node, options) ? hardline : line, - ]); + return [printAssignment(path, options, print, parts, " =", "value"), semi]; } module.exports = { diff --git a/src/language-js/print/comment.js b/src/language-js/print/comment.js index a1de75358..b972d8fa4 100644 --- a/src/language-js/print/comment.js +++ b/src/language-js/print/comment.js @@ -3,6 +3,7 @@ const { hasNewline } = require("../../common/util"); const { builders: { join, hardline }, + utils: { replaceNewlinesWithLiterallines }, } = require("../../document"); const { isLineComment, isBlockComment } = require("../utils"); @@ -38,7 +39,11 @@ function printComment(commentPath, options) { const commentEnd = locEnd(comment); const isInsideFlowComment = options.originalText.slice(commentEnd - 3, commentEnd) === "*-/"; - return "/*" + comment.value + (isInsideFlowComment ? "*-/" : "*/"); + return [ + "/*", + replaceNewlinesWithLiterallines(comment.value), + isInsideFlowComment ? "*-/" : "*/", + ]; } /* istanbul ignore next */ diff --git a/src/language-js/print/decorators.js b/src/language-js/print/decorators.js new file mode 100644 index 000000000..db2ecd1d2 --- /dev/null +++ b/src/language-js/print/decorators.js @@ -0,0 +1,85 @@ +"use strict"; + +const { isNonEmptyArray, hasNewline } = require("../../common/util"); +const { + builders: { line, hardline, join, breakParent, group }, +} = require("../../document"); +const { locStart, locEnd } = require("../loc"); +const { getParentExportDeclaration } = require("../utils"); + +function printClassMemberDecorators(path, options, print) { + const node = path.getValue(); + return group([ + join(line, path.map(print, "decorators")), + hasNewlineBetweenOrAfterDecorators(node, options) ? hardline : line, + ]); +} + +function printDecoratorsBeforeExport(path, options, print) { + // Export declarations are responsible for printing any decorators + // that logically apply to node.declaration. + return [ + join(hardline, path.map(print, "declaration", "decorators")), + hardline, + ]; +} + +function printDecorators(path, options, print) { + const node = path.getValue(); + const { decorators } = node; + + if ( + !isNonEmptyArray(decorators) || + // If the parent node is an export declaration and the decorator + // was written before the export, the export will be responsible + // for printing the decorators. + hasDecoratorsBeforeExport(path.getParentNode()) + ) { + return; + } + + const shouldBreak = + node.type === "ClassExpression" || + node.type === "ClassDeclaration" || + hasNewlineBetweenOrAfterDecorators(node, options); + + return [ + getParentExportDeclaration(path) + ? hardline + : shouldBreak + ? breakParent + : "", + join(line, path.map(print, "decorators")), + line, + ]; +} + +function hasNewlineBetweenOrAfterDecorators(node, options) { + return node.decorators.some((decorator) => + hasNewline(options.originalText, locEnd(decorator)) + ); +} + +function hasDecoratorsBeforeExport(node) { + if ( + node.type !== "ExportDefaultDeclaration" && + node.type !== "ExportNamedDeclaration" && + node.type !== "DeclareExportDeclaration" + ) { + return false; + } + + const decorators = node.declaration && node.declaration.decorators; + + return ( + isNonEmptyArray(decorators) && + locStart(node, { ignoreDecorators: true }) > locStart(decorators[0]) + ); +} + +module.exports = { + printDecorators, + printClassMemberDecorators, + printDecoratorsBeforeExport, + hasDecoratorsBeforeExport, +}; diff --git a/src/language-js/print/function-parameters.js b/src/language-js/print/function-parameters.js index 6841d282f..7a5b0150d 100644 --- a/src/language-js/print/function-parameters.js +++ b/src/language-js/print/function-parameters.js @@ -4,7 +4,7 @@ const { getNextNonSpaceNonCommentCharacter } = require("../../common/util"); const { printDanglingComments } = require("../../main/comments"); const { builders: { line, hardline, softline, group, indent, ifBreak }, - utils: { removeLines }, + utils: { removeLines, willBreak }, } = require("../../document"); const { getFunctionParameters, @@ -189,4 +189,48 @@ function shouldHugFunctionParameters(node) { ); } -module.exports = { printFunctionParameters, shouldHugFunctionParameters }; +function getReturnTypeNode(functionNode) { + let returnTypeNode; + if (functionNode.returnType) { + returnTypeNode = functionNode.returnType; + if (returnTypeNode.typeAnnotation) { + returnTypeNode = returnTypeNode.typeAnnotation; + } + } else if (functionNode.typeAnnotation) { + returnTypeNode = functionNode.typeAnnotation; + } + return returnTypeNode; +} + +// When parameters are grouped, the return type annotation breaks first. +function shouldGroupFunctionParameters(functionNode, returnTypeDoc) { + const returnTypeNode = getReturnTypeNode(functionNode); + if (!returnTypeNode) { + return false; + } + + const typeParameters = + functionNode.typeParameters && functionNode.typeParameters.params; + if (typeParameters) { + if (typeParameters.length > 1) { + return false; + } + if (typeParameters.length === 1) { + const typeParameter = typeParameters[0]; + if (typeParameter.constraint || typeParameter.default) { + return false; + } + } + } + + return ( + getFunctionParameters(functionNode).length === 1 && + (isObjectType(returnTypeNode) || willBreak(returnTypeDoc)) + ); +} + +module.exports = { + printFunctionParameters, + shouldHugFunctionParameters, + shouldGroupFunctionParameters, +}; diff --git a/src/language-js/print/function.js b/src/language-js/print/function.js index 745bfe57d..1cd092e42 100644 --- a/src/language-js/print/function.js +++ b/src/language-js/print/function.js @@ -25,7 +25,10 @@ const { CommentCheckFlags, } = require("../utils"); const { locEnd } = require("../loc"); -const { printFunctionParameters } = require("./function-parameters"); +const { + printFunctionParameters, + shouldGroupFunctionParameters, +} = require("./function-parameters"); const { printPropertyKey } = require("./property"); const { printFunctionTypeParameters } = require("./misc"); @@ -53,11 +56,20 @@ function printFunctionDeclaration(path, print, options, expandArg) { parts.push(path.call(print, "id")); } + const parametersDoc = printFunctionParameters( + path, + print, + options, + expandArg + ); + const returnTypeDoc = printReturnType(path, print, options); + const shouldGroupParameters = shouldGroupFunctionParameters(n, returnTypeDoc); + parts.push( printFunctionTypeParameters(path, options, print), group([ - printFunctionParameters(path, print, options, expandArg), - printReturnType(path, print, options), + shouldGroupParameters ? group(parametersDoc) : parametersDoc, + returnTypeDoc, ]), n.body ? " " : "", path.call(print, "body") @@ -110,15 +122,22 @@ function printMethod(path, options, print) { } function printMethodInternal(path, options, print) { + const node = path.getNode(); + const parametersDoc = printFunctionParameters(path, print, options); + const returnTypeDoc = printReturnType(path, print, options); + const shouldGroupParameters = shouldGroupFunctionParameters( + node, + returnTypeDoc + ); const parts = [ printFunctionTypeParameters(path, options, print), group([ - printFunctionParameters(path, print, options), - printReturnType(path, print, options), + shouldGroupParameters ? group(parametersDoc) : parametersDoc, + returnTypeDoc, ]), ]; - if (path.getNode().body) { + if (node.body) { parts.push(" ", path.call(print, "body")); } else { parts.push(options.semi ? ";" : ""); diff --git a/src/language-js/print/interface.js b/src/language-js/print/interface.js index c6811babe..85aca9418 100644 --- a/src/language-js/print/interface.js +++ b/src/language-js/print/interface.js @@ -60,15 +60,10 @@ function printInterface(path, options, print) { (n.id && hasComment(n.id, CommentCheckFlags.Trailing)) || isNonEmptyArray(n.extends) ) { - const printedExtends = extendsParts; if (shouldIndentOnlyHeritageClauses) { - parts.push( - group( - partsGroup.concat(ifBreak(indent(printedExtends), printedExtends)) - ) - ); + parts.push(group([...partsGroup, indent(extendsParts)])); } else { - parts.push(group(indent(partsGroup.concat(printedExtends)))); + parts.push(group(indent([...partsGroup, ...extendsParts]))); } } else { parts.push(...partsGroup, ...extendsParts); diff --git a/src/language-js/print/jsx.js b/src/language-js/print/jsx.js index 1d619a6f6..9d2654c25 100644 --- a/src/language-js/print/jsx.js +++ b/src/language-js/print/jsx.js @@ -14,7 +14,7 @@ const { lineSuffixBoundary, join, }, - utils: { willBreak, isLineNext, isEmpty }, + utils: { willBreak }, } = require("../../document"); const { getLast, getPreferredQuote } = require("../../common/util"); @@ -22,7 +22,7 @@ const { isJsxNode, rawText, isLiteral, - isCallOrOptionalCallExpression, + isCallExpression, isStringLiteral, isBinaryish, hasComment, @@ -32,8 +32,11 @@ const { const pathNeedsParens = require("../needs-parens"); const { willPrintOwnComments } = require("../comments"); +const isEmptyStringOrAnyLine = (doc) => + doc === "" || doc === line || doc === hardline || doc === softline; + /** - * @typedef {import("../../common/fast-path")} FastPath + * @typedef {import("../../common/ast-path")} AstPath * @typedef {import("../types/estree").Node} Node * @typedef {import("../types/estree").JSXElement} JSXElement */ @@ -175,18 +178,15 @@ function printJsxElementInternal(path, options, print) { } // Trim trailing lines (or empty strings) - while ( - children.length > 0 && - (isLineNext(getLast(children)) || isEmpty(getLast(children))) - ) { + while (children.length > 0 && isEmptyStringOrAnyLine(getLast(children))) { children.pop(); } // Trim leading lines (or empty strings) while ( - children.length > 0 && - (isLineNext(children[0]) || isEmpty(children[0])) && - (isLineNext(children[1]) || isEmpty(children[1])) + children.length > 1 && + isEmptyStringOrAnyLine(children[0]) && + isEmptyStringOrAnyLine(children[1]) ) { children.shift(); children.shift(); @@ -451,7 +451,7 @@ function maybeWrapJsxElementInParens(path, elem, options) { const shouldBreak = path.match( undefined, (node) => node.type === "ArrowFunctionExpression", - isCallOrOptionalCallExpression, + isCallExpression, (node) => node.type === "JSXExpressionContainer" ); @@ -505,8 +505,7 @@ function printJsxExpressionContainer(path, options, print) { (n.expression.type === "ArrayExpression" || n.expression.type === "ObjectExpression" || n.expression.type === "ArrowFunctionExpression" || - n.expression.type === "CallExpression" || - n.expression.type === "OptionalCallExpression" || + isCallExpression(n.expression) || n.expression.type === "FunctionExpression" || n.expression.type === "TemplateLiteral" || n.expression.type === "TaggedTemplateExpression" || @@ -668,7 +667,7 @@ function printJsxOpeningClosingFragment(path, options /*, print*/) { function printJsxElement(path, options, print) { const elem = printComments( path, - () => printJsxElementInternal(path, options, print), + printJsxElementInternal(path, options, print), options ); return maybeWrapJsxElementInParens(path, elem, options); @@ -697,7 +696,7 @@ function printJsxSpreadAttribute(path, options, print) { return printed; } return [ - indent([softline, printComments(p, () => printed, options)]), + indent([softline, printComments(p, printed, options)]), softline, ]; }, @@ -713,7 +712,7 @@ function printJsx(path, options, print) { case "JSXAttribute": return printJsxAttribute(path, options, print); case "JSXIdentifier": - return "" + n.name; + return String(n.name); case "JSXNamespacedName": return join(":", [ path.call(print, "namespace"), @@ -813,7 +812,7 @@ function isJsxWhitespaceExpression(node) { } /** - * @param {FastPath} path + * @param {AstPath} path * @returns {boolean} */ function hasJsxIgnoreComment(path) { diff --git a/src/language-js/print/literal.js b/src/language-js/print/literal.js new file mode 100644 index 000000000..e05aa6165 --- /dev/null +++ b/src/language-js/print/literal.js @@ -0,0 +1,58 @@ +"use strict"; +const { printString, printNumber } = require("../../common/util"); + +function printLiteral(path, options /*, print*/) { + const node = path.getNode(); + + switch (node.type) { + case "RegExpLiteral": // Babel 6 Literal split + return printRegex(node); + case "BigIntLiteral": + // babel: node.extra.raw, flow: node.bigint + return printBigInt(node.bigint || node.extra.raw); + case "NumericLiteral": // Babel 6 Literal split + return printNumber(node.extra.raw); + case "StringLiteral": // Babel 6 Literal split + return printString(node.extra.raw, options); + case "NullLiteral": // Babel 6 Literal split + return "null"; + case "BooleanLiteral": // Babel 6 Literal split + return String(node.value); + case "DecimalLiteral": + return printNumber(node.value) + "m"; + case "Literal": { + if (node.regex) { + return printRegex(node.regex); + } + + if (node.bigint) { + return printBigInt(node.raw); + } + + const { value } = node; + + if (typeof value === "number") { + return printNumber(node.raw); + } + + if (typeof value === "string") { + return printString(node.raw, options); + } + + return String(value); + } + } +} + +function printBigInt(raw) { + return raw.toLowerCase(); +} + +function printRegex({ pattern, flags }) { + flags = flags.split("").sort().join(""); + return `/${pattern}/${flags}`; +} + +module.exports = { + printLiteral, +}; diff --git a/src/language-js/print/member-chain.js b/src/language-js/print/member-chain.js index b12c6c191..7b59095c0 100644 --- a/src/language-js/print/member-chain.js +++ b/src/language-js/print/member-chain.js @@ -10,7 +10,8 @@ const { } = require("../../common/util"); const pathNeedsParens = require("../needs-parens"); const { - isCallOrOptionalCallExpression, + isCallExpression, + isMemberExpression, isFunctionOrArrowExpression, isLongCurriedCallExpression, isMemberish, @@ -23,7 +24,15 @@ const { const { locEnd } = require("../loc"); const { - builders: { join, hardline, group, indent, conditionalGroup, breakParent }, + builders: { + join, + hardline, + group, + indent, + conditionalGroup, + breakParent, + label, + }, utils: { willBreak }, } = require("../../document"); const printCallArguments = require("./call-arguments"); @@ -85,15 +94,15 @@ function printMemberChain(path, options, print) { function rec(path) { const node = path.getValue(); if ( - isCallOrOptionalCallExpression(node) && - (isMemberish(node.callee) || isCallOrOptionalCallExpression(node.callee)) + isCallExpression(node) && + (isMemberish(node.callee) || isCallExpression(node.callee)) ) { printedNodes.unshift({ node, printed: [ printComments( path, - () => [ + [ printOptionalToken(path), printFunctionTypeParameters(path, options, print), printCallArguments(path, options, print), @@ -110,11 +119,9 @@ function printMemberChain(path, options, print) { needsParens: pathNeedsParens(path, options), printed: printComments( path, - () => - node.type === "OptionalMemberExpression" || - node.type === "MemberExpression" - ? printMemberLookup(path, options, print) - : printBindExpressionCallee(path, options, print), + isMemberExpression(node) + ? printMemberLookup(path, options, print) + : printBindExpressionCallee(path, options, print), options ), }); @@ -122,7 +129,7 @@ function printMemberChain(path, options, print) { } else if (node.type === "TSNonNullExpression") { printedNodes.unshift({ node, - printed: printComments(path, () => "!", options), + printed: printComments(path, "!", options), }); path.call((expression) => rec(expression), "expression"); } else { @@ -179,9 +186,8 @@ function printMemberChain(path, options, print) { for (; i < printedNodes.length; ++i) { if ( printedNodes[i].node.type === "TSNonNullExpression" || - isCallOrOptionalCallExpression(printedNodes[i].node) || - ((printedNodes[i].node.type === "MemberExpression" || - printedNodes[i].node.type === "OptionalMemberExpression") && + isCallExpression(printedNodes[i].node) || + (isMemberExpression(printedNodes[i].node) && printedNodes[i].node.computed && isNumericLiteral(printedNodes[i].node.property)) ) { @@ -190,7 +196,7 @@ function printMemberChain(path, options, print) { break; } } - if (!isCallOrOptionalCallExpression(printedNodes[0].node)) { + if (!isCallExpression(printedNodes[0].node)) { for (; i + 1 < printedNodes.length; ++i) { if ( isMemberish(printedNodes[i].node) && @@ -228,7 +234,7 @@ function printMemberChain(path, options, print) { } if ( - isCallOrOptionalCallExpression(printedNodes[i].node) || + isCallExpression(printedNodes[i].node) || printedNodes[i].node.type === "ImportExpression" ) { hasSeenCallExpression = true; @@ -288,8 +294,7 @@ function printMemberChain(path, options, print) { const lastNode = getLast(groups[0]).node; return ( - (lastNode.type === "MemberExpression" || - lastNode.type === "OptionalMemberExpression") && + isMemberExpression(lastNode) && lastNode.property.type === "Identifier" && (isFactory(lastNode.property.name) || hasComputed) ); @@ -350,7 +355,7 @@ function printMemberChain(path, options, print) { // empty line after const lastNodeBeforeIndent = getLast(groups[shouldMerge ? 1 : 0]).node; const shouldHaveEmptyLineBeforeIndent = - !isCallOrOptionalCallExpression(lastNodeBeforeIndent) && + !isCallExpression(lastNodeBeforeIndent) && shouldInsertEmptyLineAfter(lastNodeBeforeIndent); const expanded = [ @@ -362,13 +367,13 @@ function printMemberChain(path, options, print) { const callExpressions = printedNodes .map(({ node }) => node) - .filter(isCallOrOptionalCallExpression); + .filter(isCallExpression); function lastGroupWillBreakAndOtherCallsHaveFunctionArguments() { const lastGroupNode = getLast(getLast(groups)).node; const lastGroupDoc = getLast(printedGroups); return ( - isCallOrOptionalCallExpression(lastGroupNode) && + isCallExpression(lastGroupNode) && willBreak(lastGroupDoc) && callExpressions .slice(0, -1) @@ -376,6 +381,8 @@ function printMemberChain(path, options, print) { ); } + let result; + // We don't want to print in one line if at least one of these conditions occurs: // * the chain has comments, // * the chain is an expression statement and all the arguments are literal-like ("fluent configuration" pattern), @@ -391,16 +398,18 @@ function printMemberChain(path, options, print) { printedGroups.slice(0, -1).some(willBreak) || lastGroupWillBreakAndOtherCallsHaveFunctionArguments() ) { - return group(expanded); + result = group(expanded); + } else { + result = [ + // We only need to check `oneLine` because if `expanded` is chosen + // that means that the parent group has already been broken + // naturally + willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "", + conditionalGroup([oneLine, expanded]), + ]; } - return [ - // We only need to check `oneLine` because if `expanded` is chosen - // that means that the parent group has already been broken - // naturally - willBreak(oneLine) || shouldHaveEmptyLineBeforeIndent ? breakParent : "", - conditionalGroup([oneLine, expanded]), - ]; + return label("member-chain", result); } module.exports = printMemberChain; diff --git a/src/language-js/print/member.js b/src/language-js/print/member.js index 01cc8b1eb..8b196b592 100644 --- a/src/language-js/print/member.js +++ b/src/language-js/print/member.js @@ -3,7 +3,7 @@ const { builders: { softline, group, indent }, } = require("../../document"); -const { isNumericLiteral } = require("../utils"); +const { isNumericLiteral, isMemberExpression } = require("../utils"); const { printOptionalToken } = require("./misc"); function printMemberExpression(path, options, print) { @@ -17,8 +17,7 @@ function printMemberExpression(path, options, print) { i++; } while ( firstNonMemberParent && - (firstNonMemberParent.type === "MemberExpression" || - firstNonMemberParent.type === "OptionalMemberExpression" || + (isMemberExpression(firstNonMemberParent) || firstNonMemberParent.type === "TSNonNullExpression") ); @@ -26,15 +25,12 @@ function printMemberExpression(path, options, print) { (firstNonMemberParent && (firstNonMemberParent.type === "NewExpression" || firstNonMemberParent.type === "BindExpression" || - (firstNonMemberParent.type === "VariableDeclarator" && - firstNonMemberParent.id.type !== "Identifier") || (firstNonMemberParent.type === "AssignmentExpression" && firstNonMemberParent.left.type !== "Identifier"))) || n.computed || (n.object.type === "Identifier" && n.property.type === "Identifier" && - parent.type !== "MemberExpression" && - parent.type !== "OptionalMemberExpression"); + !isMemberExpression(parent)); return [ path.call(print, "object"), diff --git a/src/language-js/print/module.js b/src/language-js/print/module.js index 766db9c28..4719e3880 100644 --- a/src/language-js/print/module.js +++ b/src/language-js/print/module.js @@ -13,6 +13,10 @@ const { needsHardlineAfterDanglingComment, } = require("../utils"); const { locStart, hasSameLoc } = require("../loc"); +const { + hasDecoratorsBeforeExport, + printDecoratorsBeforeExport, +} = require("./decorators"); /** * @typedef {import("../../document").Doc} Doc @@ -47,6 +51,12 @@ function printExportDeclaration(path, options, print) { /** @type{Doc[]} */ const parts = []; + // Only print decorators here if they were written before the export, + // otherwise they are printed by the node.declaration + if (hasDecoratorsBeforeExport(node)) { + parts.push(printDecoratorsBeforeExport(path, options, print)); + } + const { type, exportKind, declaration } = node; parts.push("export"); diff --git a/src/language-js/print/object.js b/src/language-js/print/object.js index e5c2b387d..c8ff9dc45 100644 --- a/src/language-js/print/object.js +++ b/src/language-js/print/object.js @@ -113,7 +113,7 @@ function printObject(path, options, print) { const props = propsAndLoc .sort((a, b) => a.loc - b.loc) .map((prop) => { - const result = separatorParts.concat(group(prop.printed)); + const result = [...separatorParts, group(prop.printed)]; separatorParts = [separator, line]; if ( (prop.node.type === "TSPropertySignature" || @@ -147,9 +147,9 @@ function printObject(path, options, print) { "...", ]; } else { - printed = "..."; + printed = ["..."]; } - props.push(separatorParts.concat(printed)); + props.push([...separatorParts, ...printed]); } const lastElem = getLast(n[propertiesField]); diff --git a/src/language-js/print/property.js b/src/language-js/print/property.js index e6f8c21d2..bca524d7b 100644 --- a/src/language-js/print/property.js +++ b/src/language-js/print/property.js @@ -64,10 +64,7 @@ function printPropertyKey(path, options, print) { ), options ); - return path.call( - (keyPath) => printComments(keyPath, () => prop, options), - "key" - ); + return path.call((keyPath) => printComments(keyPath, prop, options), "key"); } if ( @@ -82,7 +79,7 @@ function printPropertyKey(path, options, print) { (keyPath) => printComments( keyPath, - () => (/^\d/.test(key.value) ? printNumber(key.value) : key.value), + /^\d/.test(key.value) ? printNumber(key.value) : key.value, options ), "key" @@ -99,12 +96,12 @@ function printProperty(path, options, print) { } return printAssignment( - n.key, + path, + options, + print, printPropertyKey(path, options, print), ":", - n.value, - path.call(print, "value"), - options + "value" ); } diff --git a/src/language-js/print/statement.js b/src/language-js/print/statement.js index 8c15f1e1b..a737ed5af 100644 --- a/src/language-js/print/statement.js +++ b/src/language-js/print/statement.js @@ -17,7 +17,7 @@ const { shouldPrintParamsWithoutParens } = require("./function"); /** * @typedef {import("../../document").Doc} Doc - * @typedef {import("../../common/fast-path")} FastPath + * @typedef {import("../../common/ast-path")} AstPath */ function printStatementSequence(path, options, print, property) { @@ -54,19 +54,15 @@ function printStatementSequence(path, options, print, property) { parts.push(printed); } - if (!options.semi && isClassBody) { - if (classPropMayCauseASIProblems(node)) { - parts.push(";"); - } else if ( - node.type === "ClassProperty" || - node.type === "PropertyDefinition" - ) { - // `ClassBody` don't allow `EmptyStatement`, - // so we can use `statements` to get next node - if (classChildNeedsASIProtection(statements[index + 1])) { - parts.push(";"); - } - } + if ( + !options.semi && + isClassBody && + isClassProperty(node) && + // `ClassBody` don't allow `EmptyStatement`, + // so we can use `statements` to get next node + shouldPrintSemicolonAfterClassProperty(node, statements[index + 1]) + ) { + parts.push(";"); } if (node !== lastStatement) { @@ -105,27 +101,48 @@ function statementNeedsASIProtection(path, options) { function expressionNeedsASIProtection(path, options) { const node = path.getValue(); + switch (node.type) { + case "ParenthesizedExpression": + case "TypeCastExpression": + case "ArrayExpression": + case "ArrayPattern": + case "TemplateLiteral": + case "TemplateElement": + case "RegExpLiteral": + return true; + case "ArrowFunctionExpression": { + if (!shouldPrintParamsWithoutParens(path, options)) { + return true; + } + break; + } + case "UnaryExpression": { + const { prefix, operator } = node; + if (prefix && (operator === "+" || operator === "-")) { + return true; + } + break; + } + case "BindExpression": { + if (!node.object) { + return true; + } + break; + } + case "Literal": { + if (node.regex) { + return true; + } + break; + } + default: { + if (isJsxNode(node)) { + return true; + } + } + } - const maybeASIProblem = - pathNeedsParens(path, options) || - node.type === "ParenthesizedExpression" || - node.type === "TypeCastExpression" || - (node.type === "ArrowFunctionExpression" && - !shouldPrintParamsWithoutParens(path, options)) || - node.type === "ArrayExpression" || - node.type === "ArrayPattern" || - (node.type === "UnaryExpression" && - node.prefix && - (node.operator === "+" || node.operator === "-")) || - node.type === "TemplateLiteral" || - node.type === "TemplateElement" || - isJsxNode(node) || - (node.type === "BindExpression" && !node.object) || - node.type === "RegExpLiteral" || - (node.type === "Literal" && node.pattern) || - (node.type === "Literal" && node.regex); - - if (maybeASIProblem) { + if (pathNeedsParens(path, options)) { return true; } @@ -147,16 +164,15 @@ function printSwitchCaseConsequent(path, options, print) { return printStatementSequence(path, options, print, "consequent"); } +const isClassProperty = ({ type }) => + type === "ClassProperty" || + type === "PropertyDefinition" || + type === "ClassPrivateProperty"; /** * @returns {boolean} */ -function classPropMayCauseASIProblems(node) { - if (node.type !== "ClassProperty" && node.type !== "PropertyDefinition") { - return false; - } - +function shouldPrintSemicolonAfterClassProperty(node, nextNode) { const name = node.key && node.key.name; - // this isn't actually possible yet with most parsers available today // so isn't properly tested yet. if ( @@ -167,50 +183,49 @@ function classPropMayCauseASIProblems(node) { return true; } - return false; -} - -/** - * @returns {boolean} - */ -function classChildNeedsASIProtection(node) { - if (!node) { - return; + if (!nextNode) { + return false; } if ( - node.static || - node.accessibility // TypeScript + nextNode.static || + nextNode.accessibility // TypeScript ) { return false; } - if (!node.computed) { - const name = node.key && node.key.name; + if (!nextNode.computed) { + const name = nextNode.key && nextNode.key.name; if (name === "in" || name === "instanceof") { return true; } } - switch (node.type) { + + switch (nextNode.type) { case "ClassProperty": case "PropertyDefinition": case "TSAbstractClassProperty": - return node.computed; + return nextNode.computed; case "MethodDefinition": // Flow case "TSAbstractMethodDefinition": // TypeScript case "ClassMethod": case "ClassPrivateMethod": { // Babel - const isAsync = node.value ? node.value.async : node.async; - const isGenerator = node.value ? node.value.generator : node.generator; - if (isAsync || node.kind === "get" || node.kind === "set") { + const isAsync = nextNode.value ? nextNode.value.async : nextNode.async; + if (isAsync || nextNode.kind === "get" || nextNode.kind === "set") { return false; } - if (node.computed || isGenerator) { + + const isGenerator = nextNode.value + ? nextNode.value.generator + : nextNode.generator; + if (nextNode.computed || isGenerator) { return true; } + return false; } + case "TSIndexSignature": return true; } diff --git a/src/language-js/print/template-literal.js b/src/language-js/print/template-literal.js index 23e5d5692..9e7833296 100644 --- a/src/language-js/print/template-literal.js +++ b/src/language-js/print/template-literal.js @@ -20,6 +20,7 @@ const { isJestEachTemplateLiteral, isSimpleTemplateLiteral, hasComment, + isMemberExpression, } = require("../utils"); function printTemplateLiteral(path, print, options) { @@ -85,8 +86,7 @@ function printTemplateLiteral(path, print, options) { // in the middle of a MemberExpression if ( hasComment(expression) || - expression.type === "MemberExpression" || - expression.type === "OptionalMemberExpression" || + isMemberExpression(expression) || expression.type === "ConditionalExpression" || expression.type === "SequenceExpression" || expression.type === "TSAsExpression" || diff --git a/src/language-js/print/ternary.js b/src/language-js/print/ternary.js index bcbf7315f..91202114f 100644 --- a/src/language-js/print/ternary.js +++ b/src/language-js/print/ternary.js @@ -2,7 +2,13 @@ const flat = require("lodash/flatten"); const { hasNewlineInRange } = require("../../common/util"); -const { isJsxNode, isBlockComment, getComments } = require("../utils"); +const { + isJsxNode, + isBlockComment, + getComments, + isCallExpression, + isMemberExpression, +} = require("../utils"); const { locStart, locEnd } = require("../loc"); const { builders: { @@ -19,7 +25,7 @@ const { /** * @typedef {import("../../document").Doc} Doc - * @typedef {import("../../common/fast-path")} FastPath + * @typedef {import("../../common/ast-path")} AstPath * * @typedef {any} Options - Prettier options (TBD ...) */ @@ -52,10 +58,7 @@ const { // } // } // } -// -// We want to traverse over that shape and convert it into a flat structure so -// that we can find if there's a JSXElement somewhere inside. -function getConditionalChainContents(node) { +function conditionalExpressionChainContainsJsx(node) { // Given this code: // // // Using a ConditionalExpression as the consequent is uncommon, but should @@ -82,41 +85,25 @@ function getConditionalChainContents(node) { // } // } // - // we should return this Array: - // - // [ - // Identifier(A), - // Identifier(B), - // Identifier(C), - // Identifier(D), - // Identifier(E), - // Identifier(F), - // Identifier(G), - // Identifier(H), - // Identifier(I) - // ]; - // - // This loses the information about whether each node was the test, - // consequent, or alternate, but we don't care about that here- we are only - // flattening this structure to find if there's any JSXElements inside. - const nonConditionalExpressions = []; + // We don't care about whether each node was the test, consequent, or alternate + // We are only checking if there's any JSXElements inside. + const conditionalExpressions = [node]; + for (let index = 0; index < conditionalExpressions.length; index++) { + const conditionalExpression = conditionalExpressions[index]; + for (const property of ["test", "consequent", "alternate"]) { + const node = conditionalExpression[property]; - function recurse(node) { - if (node.type === "ConditionalExpression") { - recurse(node.test); - recurse(node.consequent); - recurse(node.alternate); - } else { - nonConditionalExpressions.push(node); + if (isJsxNode(node)) { + return true; + } + + if (node.type === "ConditionalExpression") { + conditionalExpressions.push(node); + } } } - recurse(node); - return nonConditionalExpressions; -} - -function conditionalExpressionChainContainsJsx(node) { - return getConditionalChainContents(node).some(isJsxNode); + return false; } function printTernaryTest(path, options, print) { @@ -153,11 +140,60 @@ function printTernaryTest(path, options, print) { return printed; } +const ancestorNameMap = new Map([ + ["AssignmentExpression", "right"], + ["VariableDeclarator", "init"], + ["ReturnStatement", "argument"], + ["ThrowStatement", "argument"], + ["UnaryExpression", "argument"], + ["YieldExpression", "argument"], +]); +function shouldExtraIndentForConditionalExpression(path) { + const node = path.getValue(); + if (node.type !== "ConditionalExpression") { + return false; + } + + let parent; + let child = node; + for (let ancestorCount = 0; !parent; ancestorCount++) { + const node = path.getParentNode(ancestorCount); + + if ( + (isCallExpression(node) && node.callee === child) || + (isMemberExpression(node) && node.object === child) || + (node.type === "TSNonNullExpression" && node.expression === child) + ) { + child = node; + continue; + } + + // Reached chain root + + if ( + (node.type === "NewExpression" && node.callee === child) || + (node.type === "TSAsExpression" && node.expression === child) + ) { + parent = path.getParentNode(ancestorCount + 1); + child = node; + } else { + parent = node; + } + } + + // Do not add indent to direct `ConditionalExpression` + if (child === node) { + return false; + } + + return parent[ancestorNameMap.get(parent.type)] === child; +} + /** * The following is the shared logic for * ternary operators, namely ConditionalExpression * and TSConditionalType - * @param {FastPath} path - The path to the ConditionalExpression/TSConditionalType node. + * @param {AstPath} path - The path to the ConditionalExpression/TSConditionalType node. * @param {Options} options - Prettier options * @param {Function} print - Print function to call recursively * @returns {Doc} @@ -221,10 +257,10 @@ function printTernary(path, options, print) { // parens when using ?: within JSX, because the parens are analogous to // curly braces in an if statement. const wrap = (doc) => [ - ifBreak("(", ""), + ifBreak("("), indent([softline, doc]), softline, - ifBreak(")", ""), + ifBreak(")"), ]; // The only things we don't wrap are: @@ -304,18 +340,23 @@ function printTernary(path, options, print) { // ).call() const breakClosingParen = !jsxMode && - (parent.type === "MemberExpression" || - parent.type === "OptionalMemberExpression" || + (isMemberExpression(parent) || (parent.type === "NGPipeExpression" && parent.left === node)) && !parent.computed; + const shouldExtraIndent = shouldExtraIndentForConditionalExpression(path); + const result = maybeGroup([ printTernaryTest(path, options, print), forceNoIndent ? parts : indent(parts), - isConditionalExpression && breakClosingParen ? softline : "", + isConditionalExpression && breakClosingParen && !shouldExtraIndent + ? softline + : "", ]); - return isParentTest ? group([indent([softline, result]), softline]) : result; + return isParentTest || shouldExtraIndent + ? group([indent([softline, result]), softline]) + : result; } module.exports = { printTernary }; diff --git a/src/language-js/print/type-annotation.js b/src/language-js/print/type-annotation.js index 101143a0d..59b9cbd29 100644 --- a/src/language-js/print/type-annotation.js +++ b/src/language-js/print/type-annotation.js @@ -14,8 +14,11 @@ const { isObjectTypePropertyAFunction, shouldPrintComma, } = require("../utils"); -const { printAssignmentRight } = require("./assignment"); -const { printFunctionParameters } = require("./function-parameters"); +const { printAssignment } = require("./assignment"); +const { + printFunctionParameters, + shouldGroupFunctionParameters, +} = require("./function-parameters"); const { printArrayItems } = require("./array"); function shouldHugType(node) { @@ -79,24 +82,17 @@ function printTypeAlias(path, options, print) { if (n.declare) { parts.push("declare "); } - - const printed = printAssignmentRight( - n.id, - n.right, - path.call(print, "right"), - options - ); - parts.push( "type ", path.call(print, "id"), - path.call(print, "typeParameters"), - " =", - printed, - semi + path.call(print, "typeParameters") ); - - return group(parts); + const rightPropertyName = + n.type === "TSTypeAliasDeclaration" ? "typeAnnotation" : "right"; + return [ + printAssignment(path, options, print, parts, " =", rightPropertyName), + semi, + ]; } // `TSIntersectionType` and `IntersectionTypeAnnotation` @@ -174,7 +170,7 @@ function printUnionType(path, options, print) { if (!shouldHug) { printedType = align(2, printedType); } - return printComments(typePath, () => printedType, options); + return printComments(typePath, printedType, options); }, "types"); if (shouldHug) { @@ -233,7 +229,7 @@ function printFunctionType(path, options, print) { isArrowFunctionTypeAnnotation && (parent.type === "TypeAnnotation" || parent.type === "TSTypeAnnotation"); - // Sadly we can't put it inside of FastPath::needsColon because we are + // Sadly we can't put it inside of AstPath::needsColon because we are // printing ":" as part of the expression and it would put parenthesis // around :( const needsParens = @@ -251,26 +247,34 @@ function printFunctionType(path, options, print) { parts.push("("); } - parts.push( - printFunctionParameters( - path, - print, - options, - /* expandArg */ false, - /* printTypeParams */ true - ) + const parametersDoc = printFunctionParameters( + path, + print, + options, + /* expandArg */ false, + /* printTypeParams */ true ); // The returnType is not wrapped in a TypeAnnotation, so the colon // needs to be added separately. - if (n.returnType || n.predicate || n.typeAnnotation) { - parts.push( - isArrowFunctionTypeAnnotation ? " => " : ": ", - path.call(print, "returnType"), - path.call(print, "predicate"), - path.call(print, "typeAnnotation") - ); + const returnTypeDoc = + n.returnType || n.predicate || n.typeAnnotation + ? [ + isArrowFunctionTypeAnnotation ? " => " : ": ", + path.call(print, "returnType"), + path.call(print, "predicate"), + path.call(print, "typeAnnotation"), + ] + : ""; + + const shouldGroupParameters = shouldGroupFunctionParameters(n, returnTypeDoc); + + parts.push(shouldGroupParameters ? group(parametersDoc) : parametersDoc); + + if (returnTypeDoc) { + parts.push(returnTypeDoc); } + if (needsParens) { parts.push(")"); } diff --git a/src/language-js/print/type-parameters.js b/src/language-js/print/type-parameters.js index ca1ecb06c..8b75453ae 100644 --- a/src/language-js/print/type-parameters.js +++ b/src/language-js/print/type-parameters.js @@ -30,17 +30,13 @@ function printTypeParameters(path, options, print, paramsKey) { } const grandparent = path.getNode(2); - const isParameterInTestCall = grandparent != null && isTestCall(grandparent); + const isParameterInTestCall = grandparent && isTestCall(grandparent); const shouldInline = isParameterInTestCall || n[paramsKey].length === 0 || (n[paramsKey].length === 1 && (shouldHugType(n[paramsKey][0]) || - (n[paramsKey][0].type === "GenericTypeAnnotation" && - shouldHugType(n[paramsKey][0].id)) || - (n[paramsKey][0].type === "TSTypeReference" && - shouldHugType(n[paramsKey][0].typeName)) || n[paramsKey][0].type === "NullableTypeAnnotation")); if (shouldInline) { @@ -52,17 +48,26 @@ function printTypeParameters(path, options, print, paramsKey) { ]; } + // Keep comma if the file extension is .tsx and + // has one type parameter that isn't extend with any types. + // Because, otherwise formatted result will be invalid as tsx. + const trailingComma = + n.type === "TSTypeParameterInstantiation" // https://github.com/microsoft/TypeScript/issues/21984 + ? "" + : getFunctionParameters(n).length === 1 && + isTSXFile(options) && + !n[paramsKey][0].constraint && + path.getParentNode().type === "ArrowFunctionExpression" + ? "," + : shouldPrintComma(options, "all") + ? ifBreak(",") + : ""; + return group( [ "<", indent([softline, join([",", line], path.map(print, paramsKey))]), - ifBreak( - options.parser !== "typescript" && - options.parser !== "babel-ts" && - shouldPrintComma(options, "all") - ? "," - : "" - ), + trailingComma, softline, ">", ], @@ -124,19 +129,6 @@ function printTypeParameter(path, options, print) { parts.push(" = ", path.call(print, "default")); } - // Keep comma if the file extension is .tsx and - // has one type parameter that isn't extend with any types. - // Because, otherwise formatted result will be invalid as tsx. - const grandParent = path.getNode(2); - if ( - getFunctionParameters(parent).length === 1 && - isTSXFile(options) && - !n.constraint && - grandParent.type === "ArrowFunctionExpression" - ) { - parts.push(","); - } - return parts; } diff --git a/src/language-js/print/typescript.js b/src/language-js/print/typescript.js index 331e4f732..2e8310200 100644 --- a/src/language-js/print/typescript.js +++ b/src/language-js/print/typescript.js @@ -18,12 +18,17 @@ const { isLiteral, getTypeScriptMappedTypeModifier, shouldPrintComma, + isCallExpression, + isMemberExpression, } = require("../utils"); const { locStart, locEnd } = require("../loc"); const { printOptionalToken, printTypeScriptModifiers } = require("./misc"); const { printTernary } = require("./ternary"); -const { printFunctionParameters } = require("./function-parameters"); +const { + printFunctionParameters, + shouldGroupFunctionParameters, +} = require("./function-parameters"); const { printTemplateLiteral } = require("./template-literal"); const { printArrayItems } = require("./array"); const { printObject } = require("./object"); @@ -35,9 +40,9 @@ const { const { printPropertyKey } = require("./property"); const { printFunctionDeclaration, printMethodInternal } = require("./function"); const { printInterface } = require("./interface"); -const { printAssignmentRight } = require("./assignment"); const { printBlock } = require("./block"); const { + printTypeAlias, printIntersectionType, printUnionType, printFunctionType, @@ -87,29 +92,8 @@ function printTypescript(path, options, print) { case "TSInterfaceBody": case "TSTypeLiteral": return printObject(path, options, print); - case "TSTypeAliasDeclaration": { - if (n.declare) { - parts.push("declare "); - } - - const printed = printAssignmentRight( - n.id, - n.typeAnnotation, - n.typeAnnotation && path.call(print, "typeAnnotation"), - options - ); - - parts.push( - "type ", - path.call(print, "id"), - path.call(print, "typeParameters"), - " =", - printed, - semi - ); - - return group(parts); - } + case "TSTypeAliasDeclaration": + return printTypeAlias(path, options, print); case "TSQualifiedName": return join(".", [path.call(print, "left"), path.call(print, "right")]); case "TSAbstractMethodDefinition": @@ -181,12 +165,23 @@ function printTypescript(path, options, print) { return "undefined"; case "TSUnknownKeyword": return "unknown"; - case "TSAsExpression": - return [ + case "TSIntrinsicKeyword": + return "intrinsic"; + case "TSAsExpression": { + parts.push( path.call(print, "expression"), " as ", - path.call(print, "typeAnnotation"), - ]; + path.call(print, "typeAnnotation") + ); + const parent = path.getParentNode(); + if ( + (isCallExpression(parent) && parent.callee === n) || + (isMemberExpression(parent) && parent.object === n) + ) { + return group([indent([softline, ...parts]), softline]); + } + return parts; + } case "TSArrayType": return [path.call(print, "elementType"), "[]"]; case "TSPropertySignature": { @@ -301,6 +296,9 @@ function printTypescript(path, options, print) { case "TSConstructSignatureDeclaration": case "TSCallSignatureDeclaration": case "TSConstructorType": { + if (n.type === "TSConstructorType" && n.abstract) { + parts.push("abstract "); + } if (n.type !== "TSCallSignatureDeclaration") { parts.push("new "); } @@ -348,7 +346,7 @@ function printTypescript(path, options, print) { n.optional ? getTypeScriptMappedTypeModifier(n.optional, "?") : "", n.typeAnnotation ? ": " : "", path.call(print, "typeAnnotation"), - ifBreak(semi, ""), + ifBreak(semi), ]), printDanglingComments(path, options, /* sameIndent */ true), options.bracketSpacing ? line : softline, @@ -357,33 +355,50 @@ function printTypescript(path, options, print) { { shouldBreak } ); } - case "TSMethodSignature": + case "TSMethodSignature": { parts.push( n.accessibility ? [n.accessibility, " "] : "", n.export ? "export " : "", n.static ? "static " : "", n.readonly ? "readonly " : "", + // "abstract" and "declare" are supported by only "babel-ts" + // https://github.com/prettier/prettier/issues/9760 + n.abstract ? "abstract " : "", + n.declare ? "declare " : "", n.computed ? "[" : "", path.call(print, "key"), n.computed ? "]" : "", - printOptionalToken(path), - printFunctionParameters( - path, - print, - options, - /* expandArg */ false, - /* printTypeParams */ true - ) + printOptionalToken(path) ); - if (n.returnType || n.typeAnnotation) { - parts.push( - ": ", - path.call(print, "returnType"), - path.call(print, "typeAnnotation") - ); + const parametersDoc = printFunctionParameters( + path, + print, + options, + /* expandArg */ false, + /* printTypeParams */ true + ); + + const returnTypePropertyName = n.returnType + ? "returnType" + : "typeAnnotation"; + const returnTypeNode = n[returnTypePropertyName]; + const returnTypeDoc = returnTypeNode + ? path.call(print, returnTypePropertyName) + : ""; + const shouldGroupParameters = shouldGroupFunctionParameters( + n, + returnTypeDoc + ); + + parts.push(shouldGroupParameters ? group(parametersDoc) : parametersDoc); + + if (returnTypeNode) { + parts.push(": ", group(returnTypeDoc)); } + return group(parts); + } case "TSNamespaceExportDeclaration": parts.push("export as namespace ", path.call(print, "id")); diff --git a/src/language-js/printer-estree-json.js b/src/language-js/printer-estree-json.js index b058000d4..105c2ad28 100644 --- a/src/language-js/printer-estree-json.js +++ b/src/language-js/printer-estree-json.js @@ -10,18 +10,24 @@ function genericPrint(path, options, print) { switch (node.type) { case "JsonRoot": return [path.call(print, "node"), hardline]; - case "ArrayExpression": - return node.elements.length === 0 - ? "[]" - : [ - "[", - indent([ - hardline, - join([",", hardline], path.map(print, "elements")), - ]), - hardline, - "]", - ]; + case "ArrayExpression": { + if (node.elements.length === 0) { + return "[]"; + } + + const printed = path.map( + (elementPath) => + path.getValue() === null ? "null" : print(elementPath), + "elements" + ); + + return [ + "[", + indent([hardline, join([",", hardline], printed)]), + hardline, + "]", + ]; + } case "ObjectExpression": return node.properties.length === 0 ? "{}" @@ -48,8 +54,18 @@ function genericPrint(path, options, print) { case "StringLiteral": case "NumericLiteral": return JSON.stringify(node.value); - case "Identifier": - return JSON.stringify(node.name); + case "Identifier": { + const parent = path.getParentNode(); + if (parent && parent.type === "ObjectProperty" && parent.key === node) { + return JSON.stringify(node.name); + } + return node.name; + } + case "TemplateLiteral": + // There is only one `TemplateElement` + return path.call(print, "quasis", 0); + case "TemplateElement": + return JSON.stringify(node.value.cooked); default: /* istanbul ignore next */ throw new Error("unknown type: " + JSON.stringify(node.type)); @@ -62,18 +78,37 @@ const ignoredProperties = new Set([ "extra", "loc", "comments", + "leadingComments", + "trailingComments", + "innerComments", "errors", "range", + "tokens", ]); function clean(node, newNode /*, parent*/) { const { type } = node; - if (type === "Identifier") { - return { type: "StringLiteral", value: node.name }; + // We print quoted key + if (type === "ObjectProperty" && node.key.type === "Identifier") { + newNode.key = { type: "StringLiteral", value: node.key.name }; + return; } if (type === "UnaryExpression" && node.operator === "+") { return newNode.argument; } + // We print holes in array as `null` + if (type === "ArrayExpression") { + for (const [index, element] of node.elements.entries()) { + if (element === null) { + newNode.elements.splice(index, 0, { type: "NullLiteral" }); + } + } + return; + } + // We print `TemplateLiteral` as string + if (type === "TemplateLiteral") { + return { type: "StringLiteral", value: node.quasis[0].value.cooked }; + } } clean.ignoredProperties = ignoredProperties; diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index 5253a265a..a12301870 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -7,15 +7,9 @@ const assert = require("assert"); // TODO(azz): anything that imports from main shouldn't be in a `language-*` dir. const { printDanglingComments } = require("../main/comments"); -const { - hasNewline, - printString, - printNumber, - isNonEmptyArray, -} = require("../common/util"); +const { hasNewline, printString, printNumber } = require("../common/util"); const { builders: { join, line, hardline, softline, literalline, group, indent }, - utils: { isEmpty }, } = require("../document"); const embed = require("./embed"); const clean = require("./clean"); @@ -25,12 +19,9 @@ const pathNeedsParens = require("./needs-parens"); const preprocess = require("./print-preprocess"); const { getCallArguments, - getParentExportDeclaration, hasFlowShorthandAnnotationComment, - hasNewlineBetweenOrAfterDecorators, hasComment, CommentCheckFlags, - isExportDeclaration, isFunctionNotation, isGetterOrSetter, isTheOnlyJsxElementInMarkdown, @@ -41,6 +32,8 @@ const { rawText, shouldPrintComma, hasIgnoreComment, + isCallExpression, + isMemberExpression, } = require("./utils"); const { locStart, locEnd } = require("./loc"); @@ -93,108 +86,54 @@ const { printSwitchCaseConsequent } = require("./print/statement"); const { printMemberExpression } = require("./print/member"); const { printBlock, printBlockBody } = require("./print/block"); const { printComment } = require("./print/comment"); +const { printLiteral } = require("./print/literal"); +const { printDecorators } = require("./print/decorators"); + +function genericPrint(path, options, print, args) { + const printed = printPathNoParens(path, options, print, args); + if (!printed) { + return ""; + } -function genericPrint(path, options, printPath, args) { const node = path.getValue(); - let needsParens = false; - const linesWithoutParens = printPathNoParens(path, options, printPath, args); - - if (!node || isEmpty(linesWithoutParens)) { - return linesWithoutParens; - } - - const parentExportDecl = getParentExportDeclaration(path); - const decorators = []; + const { type } = node; + // Their decorators are handled themselves, and they can't have parentheses if ( - node.type === "ClassMethod" || - node.type === "ClassPrivateMethod" || - node.type === "ClassProperty" || - node.type === "PropertyDefinition" || - node.type === "TSAbstractClassProperty" || - node.type === "ClassPrivateProperty" || - node.type === "MethodDefinition" || - node.type === "TSAbstractMethodDefinition" || - node.type === "TSDeclareMethod" + type === "ClassMethod" || + type === "ClassPrivateMethod" || + type === "ClassProperty" || + type === "PropertyDefinition" || + type === "TSAbstractClassProperty" || + type === "ClassPrivateProperty" || + type === "MethodDefinition" || + type === "TSAbstractMethodDefinition" || + type === "TSDeclareMethod" ) { - // their decorators are handled themselves - } else if ( - isNonEmptyArray(node.decorators) && - // If the parent node is an export declaration and the decorator - // was written before the export, the export will be responsible - // for printing the decorators. - !( - parentExportDecl && - locStart(parentExportDecl, { ignoreDecorators: true }) > - locStart(node.decorators[0]) - ) - ) { - const shouldBreak = - node.type === "ClassExpression" || - node.type === "ClassDeclaration" || - hasNewlineBetweenOrAfterDecorators(node, options); - - const separator = shouldBreak ? hardline : line; - - path.each((decoratorPath) => { - let decorator = decoratorPath.getValue(); - if (decorator.expression) { - decorator = decorator.expression; - } else { - decorator = decorator.callee; - } - - decorators.push(printPath(decoratorPath), separator); - }, "decorators"); - - if (parentExportDecl) { - decorators.unshift(hardline); - } - } else if ( - isExportDeclaration(node) && - node.declaration && - isNonEmptyArray(node.declaration.decorators) && - // Only print decorators here if they were written before the export, - // otherwise they are printed by the node.declaration - locStart(node, { ignoreDecorators: true }) > - locStart(node.declaration.decorators[0]) - ) { - // Export declarations are responsible for printing any decorators - // that logically apply to node.declaration. - path.each( - (decoratorPath) => { - const decorator = decoratorPath.getValue(); - const prefix = decorator.type === "Decorator" ? "" : "@"; - decorators.push(prefix, printPath(decoratorPath), hardline); - }, - "declaration", - "decorators" - ); - } else { - // Nodes with decorators can't have parentheses, so we can avoid - // computing pathNeedsParens() except in this case. - needsParens = pathNeedsParens(path, options); + return printed; } - const parts = []; - if (needsParens) { - parts.unshift("("); + const printedDecorators = printDecorators(path, options, print); + // Nodes with decorators can't have parentheses + if (printedDecorators) { + return group([...printedDecorators, printed]); } - parts.push(linesWithoutParens); + const needsParens = pathNeedsParens(path, options); - if (needsParens) { - const node = path.getValue(); - if (hasFlowShorthandAnnotationComment(node)) { - parts.push(" /*", node.trailingComments[0].value.trimStart(), "*/"); - node.trailingComments[0].printed = true; - } - - parts.push(")"); + if (!needsParens) { + return printed; } - if (decorators.length > 0) { - return group(decorators.concat(parts)); + const parts = ["(", printed]; + + if (hasFlowShorthandAnnotationComment(node)) { + const [comment] = node.trailingComments; + parts.push(" /*", comment.value.trimStart(), "*/"); + comment.printed = true; } + + parts.push(")"); + return parts; } @@ -211,6 +150,7 @@ function printPathNoParens(path, options, print, args) { } for (const printer of [ + printLiteral, printHtmlBinding, printAngular, printJsx, @@ -235,12 +175,7 @@ function printPathNoParens(path, options, print, args) { // Print @babel/parser's InterpreterDirective here so that // leading comments on the `Program` node get printed after the hashbang. if (n.program && n.program.interpreter) { - parts.push( - path.call( - (programPath) => programPath.call(print, "interpreter"), - "program" - ) - ); + parts.push(path.call(print, "program", "interpreter")); } parts.push(path.call(print, "program")); @@ -255,7 +190,7 @@ function printPathNoParens(path, options, print, args) { case "ExpressionStatement": // Detect Flow and TypeScript directives if (n.directive) { - return [nodeStr(n.expression, options, true), semi]; + return [printDirective(n.expression, options), semi]; } if (options.parser === "__vue_event_binding") { @@ -279,7 +214,10 @@ function printPathNoParens(path, options, print, args) { ]; // Babel non-standard node. Used for Closure-style type casts. See postprocess.js. case "ParenthesizedExpression": { - const shouldHug = !hasComment(n.expression); + const shouldHug = + !hasComment(n.expression) && + (n.expression.type === "ObjectExpression" || + n.expression.type === "ArrayExpression"); if (shouldHug) { return ["(", path.call(print, "expression"), ")"]; } @@ -338,15 +276,16 @@ function printPathNoParens(path, options, print, args) { printTypeAnnotation(path, options, print), ]; case "FunctionDeclaration": - case "FunctionExpression": - return printFunctionDeclaration( - path, - print, - options, - args && - args.expandLastArg && - getCallArguments(path.getParentNode()).length > 1 - ); + case "FunctionExpression": { + let expandArg = false; + if (args && args.expandLastArg) { + const parent = path.getParentNode(); + if (isCallExpression(parent) && getCallArguments(parent).length > 1) { + expandArg = true; + } + } + return printFunctionDeclaration(path, print, options, expandArg); + } case "ArrowFunctionExpression": return printArrowFunctionExpression(path, options, print, args); case "YieldExpression": @@ -364,17 +303,23 @@ function printPathNoParens(path, options, print, args) { parts.push("await"); if (n.argument) { parts.push(" ", path.call(print, "argument")); - } - const parent = path.getParentNode(); - if ( - ((parent.type === "CallExpression" || - parent.type === "OptionalCallExpression") && - parent.callee === n) || - ((parent.type === "MemberExpression" || - parent.type === "OptionalMemberExpression") && - parent.object === n) - ) { - return group([indent([softline, ...parts]), softline]); + const parent = path.getParentNode(); + if ( + (isCallExpression(parent) && parent.callee === n) || + (isMemberExpression(parent) && parent.object === n) + ) { + parts = [indent([softline, ...parts]), softline]; + const parentAwaitOrBlock = path.findAncestor( + (node) => + node.type === "AwaitExpression" || node.type === "BlockStatement" + ); + if ( + !parentAwaitOrBlock || + parentAwaitOrBlock.type !== "AwaitExpression" + ) { + return group(parts); + } + } } return parts; } @@ -435,7 +380,7 @@ function printPathNoParens(path, options, print, args) { case "ObjectMethod": return printMethod(path, options, print); case "Decorator": - return ["@", path.call(print, "expression"), path.call(print, "callee")]; + return ["@", path.call(print, "expression")]; case "ArrayExpression": case "ArrayPattern": case "TupleExpression": @@ -465,38 +410,10 @@ function printPathNoParens(path, options, print, args) { return "this"; case "Super": return "super"; - case "NullLiteral": // Babel 6 Literal split - return "null"; - case "RegExpLiteral": // Babel 6 Literal split - return printRegex(n); - case "NumericLiteral": // Babel 6 Literal split - return printNumber(n.extra.raw); - case "DecimalLiteral": - return printNumber(n.value) + "m"; - case "BigIntLiteral": - // babel: n.extra.raw, flow: n.bigint - return (n.bigint || n.extra.raw).toLowerCase(); - case "BooleanLiteral": // Babel 6 Literal split - case "StringLiteral": // Babel 6 Literal split - case "Literal": - if (n.regex) { - return printRegex(n.regex); - } - // typescript - if (n.bigint) { - return n.raw.toLowerCase(); - } - if (typeof n.value === "number") { - return printNumber(n.raw); - } - if (typeof n.value !== "string") { - return "" + n.value; - } - return nodeStr(n, options); case "Directive": return [path.call(print, "value"), semi]; // Babel 6 case "DirectiveLiteral": - return nodeStr(n, options); + return printDirective(n, options); case "UnaryExpression": parts.push(n.operator); @@ -873,7 +790,7 @@ function printPathNoParens(path, options, print, args) { case "ArrayTypeAnnotation": return [path.call(print, "elementType"), "[]"]; case "BooleanLiteralTypeAnnotation": - return "" + n.value; + return String(n.value); case "EnumDeclaration": return ["enum ", path.call(print, "id"), " ", path.call(print, "body")]; @@ -1006,12 +923,12 @@ function printPathNoParens(path, options, print, args) { case "QualifiedTypeIdentifier": return [path.call(print, "qualification"), ".", path.call(print, "id")]; case "StringLiteralTypeAnnotation": - return nodeStr(n, options); + return printString(rawText(n), options); case "NumberLiteralTypeAnnotation": assert.strictEqual(typeof n.value, "number"); // fall through case "BigIntLiteralTypeAnnotation": - if (n.extra != null) { + if (n.extra) { return printNumber(n.extra.raw); } return printNumber(n.raw); @@ -1119,16 +1036,23 @@ function printPathNoParens(path, options, print, args) { } } -function nodeStr(node, options, isFlowOrTypeScriptDirectiveLiteral) { +function printDirective(node, options) { const raw = rawText(node); - const isDirectiveLiteral = - isFlowOrTypeScriptDirectiveLiteral || node.type === "DirectiveLiteral"; - return printString(raw, options, isDirectiveLiteral); -} + const rawContent = raw.slice(1, -1); -function printRegex(node) { - const flags = node.flags.split("").sort().join(""); - return `/${node.pattern}/${flags}`; + // Check for the alternate quote, to determine if we're allowed to swap + // the quotes on a DirectiveLiteral. + if (rawContent.includes('"') || rawContent.includes("'")) { + return raw; + } + + const enclosingQuote = options.singleQuote ? "'" : '"'; + + // Directives are exact code unit sequences, which means that you can't + // change the escape sequences they use. + // See https://github.com/prettier/prettier/issues/1555 + // and https://tc39.github.io/ecma262/#directive-prologue + return enclosingQuote + rawContent + enclosingQuote; } function canAttachComment(node) { @@ -1164,6 +1088,5 @@ module.exports = { endOfLine: handleComments.handleEndOfLineComment, remaining: handleComments.handleRemainingComment, }, - getGapRegex: handleComments.getGapRegex, getCommentChildNodes: handleComments.getCommentChildNodes, }; diff --git a/src/language-js/types/estree.d.ts b/src/language-js/types/estree.d.ts index e3f9b3d3b..17187aeca 100644 --- a/src/language-js/types/estree.d.ts +++ b/src/language-js/types/estree.d.ts @@ -1,6 +1,6 @@ import * as ESTree from "estree"; import * as Babel from "@babel/types"; -import { TSESTree } from "@typescript-eslint/types"; +import { TSESTree } from "@typescript-eslint/typescript-estree"; import { ESTree as Meriyah } from "meriyah"; import * as NGTree from "angular-estree-parser/lib/types"; diff --git a/src/language-js/utils.js b/src/language-js/utils.js index 3bade04f2..a54d02de2 100644 --- a/src/language-js/utils.js +++ b/src/language-js/utils.js @@ -4,7 +4,6 @@ const isIdentifierName = require("esutils").keyword.isIdentifierNameES5; const { getLast, hasNewline, - hasNewlineInRange, skipWhitespace, isNonEmptyArray, isNextLineEmptyAfterIndex, @@ -25,7 +24,7 @@ const { locStart, locEnd, hasSameLocStart } = require("./loc"); * @typedef {import("./types/estree").TaggedTemplateExpression} TaggedTemplateExpression * @typedef {import("./types/estree").Literal} Literal * - * @typedef {import("../common/fast-path")} FastPath + * @typedef {import("../common/ast-path")} AstPath */ // We match any whitespace except line terminators because @@ -54,7 +53,7 @@ function hasFlowShorthandAnnotationComment(node) { return ( node.extra && node.extra.parenthesized && - node.trailingComments && + isNonEmptyArray(node.trailingComments) && isBlockComment(node.trailingComments[0]) && FLOW_SHORTHAND_ANNOTATION.test(node.trailingComments[0].value) ); @@ -87,7 +86,7 @@ function hasNode(node, fn) { const result = fn(node); return typeof result === "boolean" ? result - : Object.keys(node).some((key) => hasNode(node[key], fn)); + : Object.values(node).some((value) => hasNode(value, fn)); } /** @@ -101,10 +100,8 @@ function hasNakedLeftSide(node) { node.type === "LogicalExpression" || node.type === "NGPipeExpression" || node.type === "ConditionalExpression" || - node.type === "CallExpression" || - node.type === "OptionalCallExpression" || - node.type === "MemberExpression" || - node.type === "OptionalMemberExpression" || + isCallExpression(node) || + isMemberExpression(node) || node.type === "SequenceExpression" || node.type === "TaggedTemplateExpression" || node.type === "BindExpression" || @@ -203,7 +200,7 @@ function isExportDeclaration(node) { } /** - * @param {FastPath} path + * @param {AstPath} path * @returns {Node | null} */ function getParentExportDeclaration(path) { @@ -247,6 +244,14 @@ function isNumericLiteral(node) { ); } +function isSignedNumericLiteral(node) { + return ( + node.type === "UnaryExpression" && + (node.operator === "+" || node.operator === "-") && + isNumericLiteral(node.argument) + ); +} + /** * @param {Node} node * @returns {boolean} @@ -301,13 +306,12 @@ function isTemplateLiteral(node) { * Note: `inject` is used in AngularJS 1.x, `async` in Angular 2+ * example: https://docs.angularjs.org/guide/unit-testing#using-beforeall- * - * @param {Node} node + * @param {CallExpression} node * @returns {boolean} */ function isAngularTestWrapper(node) { return ( - (node.type === "CallExpression" || - node.type === "OptionalCallExpression") && + isCallExpression(node) && node.callee.type === "Identifier" && (node.callee.name === "async" || node.callee.name === "inject" || @@ -339,23 +343,6 @@ function isTheOnlyJsxElementInMarkdown(options, path) { return parent.type === "Program" && parent.body.length === 1; } -/** - * @param {Node} node - * @returns {boolean} - */ -function isMemberExpressionChain(node) { - if ( - node.type !== "MemberExpression" && - node.type !== "OptionalMemberExpression" - ) { - return false; - } - if (node.object.type === "Identifier") { - return true; - } - return isMemberExpressionChain(node.object); -} - function isGetterOrSetter(node) { return node.kind === "get" || node.kind === "set"; } @@ -415,8 +402,7 @@ function isBinaryish(node) { */ function isMemberish(node) { return ( - node.type === "MemberExpression" || - node.type === "OptionalMemberExpression" || + isMemberExpression(node) || (node.type === "BindExpression" && Boolean(node.object)) ); } @@ -492,13 +478,12 @@ function isSimpleType(node) { const unitTestRe = /^(skip|[fx]?(it|describe|test))$/; /** - * @param {CallExpression} node + * @param {{callee: MemberExpression | OptionalMemberExpression}} node * @returns {boolean} */ function isSkipOrOnlyBlock(node) { return ( - (node.callee.type === "MemberExpression" || - node.callee.type === "OptionalMemberExpression") && + isMemberExpression(node.callee) && node.callee.object.type === "Identifier" && node.callee.property.type === "Identifier" && unitTestRe.test(node.callee.object.name) && @@ -556,12 +541,25 @@ function isTestCall(n, parent) { } /** - * @param {CallExpression | OptionalCallExpression} node + * @param {Node} node * @returns {boolean} */ -function isCallOrOptionalCallExpression(node) { +function isCallExpression(node) { return ( - node.type === "CallExpression" || node.type === "OptionalCallExpression" + node && + (node.type === "CallExpression" || node.type === "OptionalCallExpression") + ); +} + +/** + * @param {Node} node + * @returns {boolean} + */ +function isMemberExpression(node) { + return ( + node && + (node.type === "MemberExpression" || + node.type === "OptionalMemberExpression") ); } @@ -593,15 +591,9 @@ function isSimpleTemplateLiteral(node) { } // Allow `a.b.c`, `a.b[c]`, and `this.x.y` - if ( - expr.type === "MemberExpression" || - expr.type === "OptionalMemberExpression" - ) { + if (isMemberExpression(expr)) { let head = expr; - while ( - head.type === "MemberExpression" || - head.type === "OptionalMemberExpression" - ) { + while (isMemberExpression(head)) { if ( head.property.type !== "Identifier" && head.property.type !== "Literal" && @@ -635,20 +627,13 @@ function isSimpleTemplateLiteral(node) { function getTypeScriptMappedTypeModifier(tokenNode, keyword) { if (tokenNode === "+") { return "+" + keyword; - } else if (tokenNode === "-") { + } + + if (tokenNode === "-") { return "-" + keyword; } - return keyword; -} -function hasNewlineBetweenOrAfterDecorators(node, options) { - return ( - hasNewlineInRange( - options.originalText, - locStart(node.decorators[0]), - locEnd(getLast(node.decorators)) - ) || hasNewline(options.originalText, locEnd(getLast(node.decorators))) - ); + return keyword; } /** @@ -839,7 +824,7 @@ function isFunctionCompositionArgs(args) { if (count > 1) { return true; } - } else if (isCallOrOptionalCallExpression(arg)) { + } else if (isCallExpression(arg)) { for (const childArg of arg.arguments) { if (isFunctionOrArrowExpression(childArg)) { return true; @@ -857,15 +842,15 @@ function isFunctionCompositionArgs(args) { // In the above call expression, the second call is the parent node and the // first call is the current node. /** - * @param {FastPath} path + * @param {AstPath} path * @returns {boolean} */ function isLongCurriedCallExpression(path) { const node = path.getValue(); const parent = path.getParentNode(); return ( - isCallOrOptionalCallExpression(node) && - isCallOrOptionalCallExpression(parent) && + isCallExpression(node) && + isCallExpression(parent) && parent.callee === node && node.arguments.length > parent.arguments.length && parent.arguments.length > 0 @@ -913,7 +898,10 @@ function isSimpleCallArgument(node, depth) { } if (node.type === "TemplateLiteral") { - return node.expressions.every(isChildSimple); + return ( + node.quasis.every((element) => !element.value.raw.includes("\n")) && + node.expressions.every(isChildSimple) + ); } if (node.type === "ObjectExpression") { @@ -930,21 +918,14 @@ function isSimpleCallArgument(node, depth) { return isChildSimple(node.source); } - if ( - node.type === "CallExpression" || - node.type === "OptionalCallExpression" || - node.type === "NewExpression" - ) { + if (isCallExpression(node) || node.type === "NewExpression") { return ( isSimpleCallArgument(node.callee, depth) && node.arguments.every(isChildSimple) ); } - if ( - node.type === "MemberExpression" || - node.type === "OptionalMemberExpression" - ) { + if (isMemberExpression(node)) { return ( isSimpleCallArgument(node.object, depth) && isSimpleCallArgument(node.property, depth) @@ -1050,6 +1031,7 @@ function startsWithNoLookaheadToken(node, forbidFunctionClassAndDoExpr) { forbidFunctionClassAndDoExpr ); case "TSAsExpression": + case "TSNonNullExpression": return startsWithNoLookaheadToken( node.expression, forbidFunctionClassAndDoExpr @@ -1152,7 +1134,7 @@ function getLeftMost(node) { function isBitwiseOperator(operator) { return ( - !!bitshiftOperators[operator] || + Boolean(bitshiftOperators[operator]) || operator === "|" || operator === "^" || operator === "&" @@ -1338,7 +1320,6 @@ module.exports = { hasFlowShorthandAnnotationComment, hasLeadingOwnLineComment, hasNakedLeftSide, - hasNewlineBetweenOrAfterDecorators, hasNode, hasIgnoreComment, hasNodeIgnoreComment, @@ -1347,7 +1328,8 @@ module.exports = { isBlockComment, isLineComment, isPrettierIgnoreComment, - isCallOrOptionalCallExpression, + isCallExpression, + isMemberExpression, isExportDeclaration, isFlowAnnotationComment, isFunctionCompositionArgs, @@ -1359,9 +1341,9 @@ module.exports = { isLiteral, isLongCurriedCallExpression, isSimpleCallArgument, - isMemberExpressionChain, isMemberish, isNumericLiteral, + isSignedNumericLiteral, isObjectType, isObjectTypePropertyAFunction, isSimpleType, diff --git a/src/language-markdown/embed.js b/src/language-markdown/embed.js index 8ca02011d..aa3641c76 100644 --- a/src/language-markdown/embed.js +++ b/src/language-markdown/embed.js @@ -8,7 +8,7 @@ const { builders: { hardline, markAsRoot }, utils: { replaceNewlinesWithLiterallines }, } = require("../document"); -const { print: printFrontMatter } = require("../utils/front-matter"); +const printFrontMatter = require("../utils/front-matter/print"); const { getFencedCodeBlockValue } = require("./utils"); function embed(path, print, textToDoc, options) { diff --git a/src/language-markdown/index.js b/src/language-markdown/index.js index 318a691a4..994023c43 100644 --- a/src/language-markdown/index.js +++ b/src/language-markdown/index.js @@ -9,7 +9,7 @@ const languages = [ since: "1.8.0", parsers: ["markdown"], vscodeLanguageIds: ["markdown"], - filenames: data.filenames.concat(["README"]), + filenames: [...data.filenames, "README"], extensions: data.extensions.filter((extension) => extension !== ".mdx"), })), createLanguage(require("linguist-languages/data/Markdown.json"), () => ({ diff --git a/src/language-markdown/parser-markdown.js b/src/language-markdown/parser-markdown.js index 67008abaf..a456c3cae 100644 --- a/src/language-markdown/parser-markdown.js +++ b/src/language-markdown/parser-markdown.js @@ -4,7 +4,7 @@ const remarkParse = require("remark-parse"); const unified = require("unified"); const remarkMath = require("remark-math"); const footnotes = require("remark-footnotes"); -const { parse: parseFrontMatter } = require("../utils/front-matter"); +const parseFrontMatter = require("../utils/front-matter/parse"); const pragma = require("./pragma"); const { locStart, locEnd } = require("./loc"); const { mapAst, INLINE_NODE_WRAPPER_TYPES } = require("./utils"); @@ -64,7 +64,7 @@ function htmlToJsx() { function frontMatter() { const proto = this.Parser.prototype; - proto.blockMethods = ["frontMatter"].concat(proto.blockMethods); + proto.blockMethods = ["frontMatter", ...proto.blockMethods]; proto.blockTokenizers.frontMatter = tokenizer; function tokenizer(eat, value) { diff --git a/src/language-markdown/pragma.js b/src/language-markdown/pragma.js index 3d5c67dfe..ef7e98042 100644 --- a/src/language-markdown/pragma.js +++ b/src/language-markdown/pragma.js @@ -1,6 +1,6 @@ "use strict"; -const { parse: parseFrontMatter } = require("../utils/front-matter"); +const parseFrontMatter = require("../utils/front-matter/parse"); const pragmas = ["format", "prettier"]; diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index 0b1b02fb1..b67adb16a 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -21,8 +21,9 @@ const { align, indent, group, + hardlineWithoutBreakParent, }, - utils: { normalizeDoc, getDocParts }, + utils: { normalizeDoc }, printer: { printDocToString }, } = require("../document"); const { replaceEndOfLineWith } = require("../common/util"); @@ -504,8 +505,6 @@ function getNthListSiblingIndex(node, parentNode) { } function getNthSiblingIndex(node, parentNode, condition) { - condition = condition || (() => true); - let index = -1; for (const childNode of parentNode.children) { @@ -522,7 +521,7 @@ function getNthSiblingIndex(node, parentNode, condition) { } function getAncestorCounter(path, typeOrTypes) { - const types = [].concat(typeOrTypes); + const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes]; let counter = -1; let ancestorNode; @@ -559,7 +558,6 @@ function printLine(path, value, options) { } function printTable(path, options, print) { - const hardlineWithoutBreakParent = getDocParts(hardline)[0]; const node = path.getValue(); const columnMaxWidths = []; @@ -698,9 +696,7 @@ function printRoot(path, options, print) { }); } -function printChildren(path, options, print, events) { - events = events || {}; - +function printChildren(path, options, print, events = {}) { const { postprocessor } = events; const processor = events.processor || ((childPath) => childPath.call(print)); @@ -840,18 +836,24 @@ function shouldRemainTheSameContent(path) { ); } -function printUrl(url, dangerousCharOrChars) { - const dangerousChars = [" "].concat(dangerousCharOrChars || []); +/** + * @param {string} url + * @param {string[] | string} [dangerousCharOrChars] + * @returns {string} + */ +function printUrl(url, dangerousCharOrChars = []) { + const dangerousChars = [ + " ", + ...(Array.isArray(dangerousCharOrChars) + ? dangerousCharOrChars + : [dangerousCharOrChars]), + ]; return new RegExp(dangerousChars.map((x) => `\\${x}`).join("|")).test(url) ? `<${url}>` : url; } -function printTitle(title, options, printSpace) { - if (printSpace == null) { - printSpace = true; - } - +function printTitle(title, options, printSpace = true) { if (!title) { return ""; } @@ -886,7 +888,7 @@ function clamp(value, min, max) { } function hasPrettierIgnore(path) { - const index = +path.getName(); + const index = Number(path.getName()); if (index === 0) { return false; diff --git a/src/language-markdown/utils.js b/src/language-markdown/utils.js index 5520e7574..de21b4e60 100644 --- a/src/language-markdown/utils.js +++ b/src/language-markdown/utils.js @@ -28,11 +28,12 @@ const INLINE_NODE_TYPES = [ "inlineMath", ]; -const INLINE_NODE_WRAPPER_TYPES = INLINE_NODE_TYPES.concat([ +const INLINE_NODE_WRAPPER_TYPES = [ + ...INLINE_NODE_TYPES, "tableCell", "paragraph", "heading", -]); +]; const kRegex = new RegExp(kPattern); const punctuationRegex = new RegExp(punctuationPattern); @@ -203,17 +204,15 @@ function getFencedCodeBlockValue(node, originalText) { function mapAst(ast, handler) { return (function preorder(node, index, parentStack) { - parentStack = parentStack || []; - const newNode = { ...handler(node, index, parentStack) }; if (newNode.children) { newNode.children = newNode.children.map((child, index) => - preorder(child, index, [newNode].concat(parentStack)) + preorder(child, index, [newNode, ...parentStack]) ); } return newNode; - })(ast, null, null); + })(ast, null, []); } function isAutolink(node) { diff --git a/src/language-yaml/print/flow-mapping-sequence.js b/src/language-yaml/print/flow-mapping-sequence.js index 28b87a059..b5d8152eb 100644 --- a/src/language-yaml/print/flow-mapping-sequence.js +++ b/src/language-yaml/print/flow-mapping-sequence.js @@ -29,7 +29,7 @@ function printFlowMapping(path, print, options) { alignWithSpaces(options.tabWidth, [ bracketSpacing, printChildren(path, print, options), - options.trailingComma === "none" ? "" : ifBreak(",", ""), + options.trailingComma === "none" ? "" : ifBreak(","), hasEndComments(node) ? [hardline, join(hardline, path.map(print, "endComments"))] : "", diff --git a/src/language-yaml/printer-yaml.js b/src/language-yaml/printer-yaml.js index bf6da30b5..36949d49c 100644 --- a/src/language-yaml/printer-yaml.js +++ b/src/language-yaml/printer-yaml.js @@ -5,7 +5,6 @@ const { builders: { breakParent, - fill, group, hardline, @@ -241,7 +240,7 @@ function printNode(node, parentNode, path, options, print) { ]; } case "directive": - return ["%", join(" ", [node.name].concat(node.parameters))]; + return ["%", join(" ", [node.name, ...node.parameters])]; case "comment": return ["#", node.value]; case "alias": @@ -285,7 +284,9 @@ function printNode(node, parentNode, path, options, print) { printFlowScalarContent(node.type, raw, options), originalQuote, ]; - } else if (raw.includes(doubleQuote)) { + } + + if (raw.includes(doubleQuote)) { return [ singleQuote, printFlowScalarContent( diff --git a/src/language-yaml/utils.js b/src/language-yaml/utils.js index a413cbf6a..2151c65bc 100644 --- a/src/language-yaml/utils.js +++ b/src/language-yaml/utils.js @@ -227,8 +227,11 @@ function getFlowScalarLineContents(nodeType, content, options) { getLast(getLast(reduced)).endsWith("\\") ) ) - ? reduced.concat([reduced.pop().concat(lineContentWords)]) - : reduced.concat([lineContentWords]), + ? [ + ...reduced.slice(0, -1), + [...getLast(reduced), ...lineContentWords], + ] + : [...reduced, lineContentWords], [] ) .map((lineContentWords) => @@ -281,8 +284,11 @@ function getBlockValueLineContents( lineContentWords.length > 0 && !/^\s/.test(lineContentWords[0]) && !/^\s|\s$/.test(getLast(reduced)) - ? reduced.concat([reduced.pop().concat(lineContentWords)]) - : reduced.concat([lineContentWords]), + ? [ + ...reduced.slice(0, -1), + [...getLast(reduced), ...lineContentWords], + ] + : [...reduced, lineContentWords], [] ) .map((lineContentWords) => @@ -290,8 +296,8 @@ function getBlockValueLineContents( (reduced, word) => // disallow trailing spaces reduced.length > 0 && /\s$/.test(getLast(reduced)) - ? reduced.concat(reduced.pop() + " " + word) - : reduced.concat(word), + ? [...reduced.slice(0, -1), getLast(reduced) + " " + word] + : [...reduced, word], [] ) ) diff --git a/src/main/ast-to-doc.js b/src/main/ast-to-doc.js index 369e6fc50..b4dde2be0 100644 --- a/src/main/ast-to-doc.js +++ b/src/main/ast-to-doc.js @@ -1,15 +1,14 @@ "use strict"; const assert = require("assert"); -const FastPath = require("../common/fast-path"); -const doc = require("../document"); +const AstPath = require("../common/ast-path"); +const { + builders: { hardline, addAlignmentToDoc }, + utils: { propagateBreaks }, +} = require("../document"); const { printComments } = require("./comments"); const multiparser = require("./multiparser"); -const docBuilders = doc.builders; -const { hardline, addAlignmentToDoc } = docBuilders; -const docUtils = doc.utils; - /** * Takes an abstract syntax tree (AST) and recursively converts it to a * document (series of printing primitives). @@ -27,7 +26,7 @@ const docUtils = doc.utils; * plugin function again, and so on. * * All the while, these functions pass a "path" variable around, which - * is a stack-like data structure (FastPath) that maintains the current + * is a stack-like data structure (AstPath) that maintains the current * state of the recursion. It is called "path", because it represents * the path to the current node through the Abstract Syntax Tree. */ @@ -48,39 +47,33 @@ function printAstToDoc(ast, options, alignmentSize = 0) { return cache.get(node); } + let doc = callPluginPrintFunction(path, options, printGenerically, args); + // We let JSXElement print its comments itself because it adds () around // UnionTypeAnnotation has to align the child without the comments - let res; if ( - printer.willPrintOwnComments && - printer.willPrintOwnComments(path, options) + !printer.willPrintOwnComments || + !printer.willPrintOwnComments(path, options) ) { - res = callPluginPrintFunction(path, options, printGenerically, args); - } else { // printComments will call the plugin print function and check for // comments to print - res = printComments( - path, - (p) => callPluginPrintFunction(p, options, printGenerically, args), - options, - args && args.needsSemi - ); + doc = printComments(path, doc, options, args && args.needsSemi); } if (shouldCache) { - cache.set(node, res); + cache.set(node, doc); } - return res; + return doc; } - let doc = printGenerically(new FastPath(ast)); + let doc = printGenerically(new AstPath(ast)); if (alignmentSize > 0) { // Add a hardline to make the indents take effect // It should be removed in index.js format() doc = addAlignmentToDoc([hardline, doc], alignmentSize, options.tabWidth); } - docUtils.propagateBreaks(doc); + propagateBreaks(doc); return doc; } @@ -106,7 +99,7 @@ function printPrettierIgnoredNode(node, options) { } function callPluginPrintFunction(path, options, printPath, args) { - assert.ok(path instanceof FastPath); + assert.ok(path instanceof AstPath); const node = path.getValue(); const { printer } = options; diff --git a/src/main/comments.js b/src/main/comments.js index a0476f577..8fd48b31e 100644 --- a/src/main/comments.js +++ b/src/main/comments.js @@ -50,16 +50,16 @@ function getSortedChildNodes(node, options, resultArray) { (printer.getCommentChildNodes && printer.getCommentChildNodes(node, options)) || (typeof node === "object" && - Object.keys(node) + Object.entries(node) .filter( - (n) => + ([n]) => n !== "enclosingNode" && n !== "precedingNode" && n !== "followingNode" && n !== "tokens" && n !== "comments" ) - .map((n) => node[n])); + .map(([, value]) => value)); if (!childNodes) { return; @@ -224,6 +224,7 @@ function attach(comments, ast, text, options) { } if (isOwnLineComment(text, options, decoratedComments, index)) { + comment.placement = "ownLine"; // If a comment exists on its own line, prefer a leading comment. // We also need to check if it's the first line of the file. if (handleOwnLineComment(...args)) { @@ -241,6 +242,7 @@ function attach(comments, ast, text, options) { addDanglingComment(ast, comment); } } else if (isEndOfLineComment(text, options, decoratedComments, index)) { + comment.placement = "endOfLine"; if (handleEndOfLineComment(...args)) { // We're good } else if (precedingNode) { @@ -257,6 +259,7 @@ function attach(comments, ast, text, options) { addDanglingComment(ast, comment); } } else { + comment.placement = "remaining"; if (handleRemainingComment(...args)) { // We're good } else if (precedingNode && followingNode) { @@ -544,9 +547,8 @@ function prependCursorPlaceholder(path, options, printed) { return printed; } -function printComments(path, print, options, needsSemi) { +function printComments(path, printed, options, needsSemi) { const value = path.getValue(); - const printed = print(path); const comments = value && value.comments; if (!isNonEmptyArray(comments)) { @@ -581,11 +583,10 @@ function printComments(path, print, options, needsSemi) { } }, "comments"); - return prependCursorPlaceholder( - path, - options, - leadingParts.concat(trailingParts) - ); + return prependCursorPlaceholder(path, options, [ + ...leadingParts, + ...trailingParts, + ]); } function ensureAllCommentsPrinted(astComments) { diff --git a/src/main/core.js b/src/main/core.js index 511df6ce9..feb3f169a 100644 --- a/src/main/core.js +++ b/src/main/core.js @@ -36,13 +36,11 @@ function attachComments(text, ast, opts) { return astComments; } -function coreFormat(originalText, opts, addAlignmentSize) { +function coreFormat(originalText, opts, addAlignmentSize = 0) { if (!originalText || originalText.trim().length === 0) { - return { formatted: "", cursorOffset: -1 }; + return { formatted: "", cursorOffset: -1, comments: [] }; } - addAlignmentSize = addAlignmentSize || 0; - const { ast, text } = parser.parse(originalText, opts); if (opts.cursorOffset >= 0) { @@ -104,6 +102,7 @@ function coreFormat(originalText, opts, addAlignmentSize) { return { formatted: result.formatted, cursorOffset: newCursorNodeStart + cursorOffsetRelativeToOldCursorNode, + comments: astComments, }; } @@ -135,10 +134,14 @@ function coreFormat(originalText, opts, addAlignmentSize) { } } - return { formatted: result.formatted, cursorOffset }; + return { formatted: result.formatted, cursorOffset, comments: astComments }; } - return { formatted: result.formatted, cursorOffset: -1 }; + return { + formatted: result.formatted, + cursorOffset: -1, + comments: astComments, + }; } function formatRange(originalText, opts) { @@ -181,8 +184,7 @@ function formatRange(originalText, opts) { let { cursorOffset } = opts; if (cursorOffset >= rangeEnd) { // handle the case where the cursor was past the end of the range - cursorOffset = - opts.cursorOffset + (rangeTrimmed.length - rangeString.length); + cursorOffset += rangeTrimmed.length - rangeString.length; } else if (rangeResult.cursorOffset >= 0) { // handle the case where the cursor was in the range cursorOffset = rangeResult.cursorOffset + rangeStart; @@ -203,7 +205,7 @@ function formatRange(originalText, opts) { formatted = formatted.replace(/\n/g, eol); } - return { formatted, cursorOffset }; + return { formatted, cursorOffset, comments: rangeResult.comments }; } function ensureIndexInText(text, index, defaultValue) { @@ -272,25 +274,25 @@ function normalizeInputAndOptions(text, options) { }; } -function format(originalText, originalOptions) { +function hasPragma(text, options) { + const selectedParser = parser.resolveParser(options); + return !selectedParser.hasPragma || selectedParser.hasPragma(text); +} + +function formatWithCursor(originalText, originalOptions) { let { hasBOM, text, options } = normalizeInputAndOptions( originalText, normalizeOptions(originalOptions) ); - if (options.rangeStart >= options.rangeEnd && text !== "") { - return { - formatted: originalText, - cursorOffset: originalOptions.cursorOffset, - }; - } - - const selectedParser = parser.resolveParser(options); - const hasPragma = !selectedParser.hasPragma || selectedParser.hasPragma(text); - if (options.requirePragma && !hasPragma) { + if ( + (options.rangeStart >= options.rangeEnd && text !== "") || + (options.requirePragma && !hasPragma(text, options)) + ) { return { formatted: originalText, cursorOffset: originalOptions.cursorOffset, + comments: [], }; } @@ -299,7 +301,12 @@ function format(originalText, originalOptions) { if (options.rangeStart > 0 || options.rangeEnd < text.length) { result = formatRange(text, options); } else { - if (!hasPragma && options.insertPragma && options.printer.insertPragma) { + if ( + !options.requirePragma && + options.insertPragma && + options.printer.insertPragma && + !hasPragma(text, options) + ) { text = options.printer.insertPragma(text); } result = coreFormat(text, options); @@ -317,7 +324,8 @@ function format(originalText, originalOptions) { } module.exports = { - formatWithCursor: format, + formatWithCursor, + parse(originalText, originalOptions, massage) { const { text, options } = normalizeInputAndOptions( originalText, @@ -338,8 +346,10 @@ module.exports = { // Doesn't handle shebang for now formatDoc(doc, options) { - return format(printDocToDebug(doc), { ...options, parser: "babel" }) - .formatted; + return formatWithCursor(printDocToDebug(doc), { + ...options, + parser: "__js_expression", + }).formatted; }, printToDoc(originalText, options) { diff --git a/src/main/massage-ast.js b/src/main/massage-ast.js index f2ce7beb7..784b568e9 100644 --- a/src/main/massage-ast.js +++ b/src/main/massage-ast.js @@ -18,9 +18,9 @@ function massageAST(ast, options, parent) { } const newObj = {}; - for (const key of Object.keys(ast)) { - if (!ignoredProperties.has(key) && typeof ast[key] !== "function") { - newObj[key] = massageAST(ast[key], options, ast); + for (const [key, value] of Object.entries(ast)) { + if (!ignoredProperties.has(key) && typeof value !== "function") { + newObj[key] = massageAST(value, options, ast); } } diff --git a/src/main/multiparser.js b/src/main/multiparser.js index 5a5153f8b..017b73447 100644 --- a/src/main/multiparser.js +++ b/src/main/multiparser.js @@ -37,13 +37,6 @@ function textToDoc( ...parentOptions, ...partialNextOptions, parentParser: parentOptions.parser, - embeddedInHtml: !!( - parentOptions.embeddedInHtml || - parentOptions.parser === "html" || - parentOptions.parser === "vue" || - parentOptions.parser === "angular" || - parentOptions.parser === "lwc" - ), originalText: text, }, { passThrough: true } diff --git a/src/main/options-normalizer.js b/src/main/options-normalizer.js index d8438aac9..4c6d0c16e 100644 --- a/src/main/options-normalizer.js +++ b/src/main/options-normalizer.js @@ -205,7 +205,7 @@ function optionInfoToSchema(optionInfo, { isCLI, optionInfos }) { return optionInfo.array ? vnopts.ArraySchema.create({ - ...(isCLI ? { preprocess: (v) => [].concat(v) } : {}), + ...(isCLI ? { preprocess: (v) => (Array.isArray(v) ? v : [v]) } : {}), ...handlers, valueSchema: SchemaConstructor.create(parameters), }) diff --git a/src/main/options.js b/src/main/options.js index 544f31e26..43a1d228e 100644 --- a/src/main/options.js +++ b/src/main/options.js @@ -18,9 +18,7 @@ const hiddenDefaults = { }; // Copy options and fill in default values. -function normalize(options, opts) { - opts = opts || {}; - +function normalize(options, opts = {}) { const rawOptions = { ...options }; const supportOptions = getSupportInfo({ @@ -70,25 +68,24 @@ function normalize(options, opts) { const plugin = getPlugin(rawOptions); rawOptions.printer = plugin.printers[rawOptions.astFormat]; - const pluginDefaults = supportOptions - .filter( - (optionInfo) => - optionInfo.pluginDefaults && - optionInfo.pluginDefaults[plugin.name] !== undefined - ) - .reduce( - (reduced, optionInfo) => - Object.assign(reduced, { - [optionInfo.name]: optionInfo.pluginDefaults[plugin.name], - }), - {} - ); + const pluginDefaults = fromPairs( + supportOptions + .filter( + (optionInfo) => + optionInfo.pluginDefaults && + optionInfo.pluginDefaults[plugin.name] !== undefined + ) + .map((optionInfo) => [ + optionInfo.name, + optionInfo.pluginDefaults[plugin.name], + ]) + ); const mixedDefaults = { ...defaults, ...pluginDefaults }; - for (const k of Object.keys(mixedDefaults)) { - if (rawOptions[k] == null) { - rawOptions[k] = mixedDefaults[k]; + for (const [k, value] of Object.entries(mixedDefaults)) { + if (rawOptions[k] === null || rawOptions[k] === undefined) { + rawOptions[k] = value; } } @@ -131,7 +128,7 @@ function getInterpreter(filepath) { let fd; try { fd = fs.openSync(filepath, "r"); - } catch (err) { + } catch { // istanbul ignore next return ""; } @@ -152,7 +149,7 @@ function getInterpreter(filepath) { return m2[1]; } return ""; - } catch (err) { + } catch { // There are some weird cases where paths are missing, causing Jest // failures. It's unclear what these correspond to in the real world. /* istanbul ignore next */ @@ -162,7 +159,7 @@ function getInterpreter(filepath) { // There are some weird cases where paths are missing, causing Jest // failures. It's unclear what these correspond to in the real world. fs.closeSync(fd); - } catch (err) { + } catch { // nop } } diff --git a/src/main/parser.js b/src/main/parser.js index ed354c653..499e1c4ad 100644 --- a/src/main/parser.js +++ b/src/main/parser.js @@ -27,9 +27,7 @@ function getParsers(options) { return parsers; } -function resolveParser(opts, parsers) { - parsers = parsers || getParsers(opts); - +function resolveParser(opts, parsers = getParsers(opts)) { if (typeof opts.parser === "function") { // Custom parser API always works with JavaScript. return { @@ -59,7 +57,7 @@ function resolveParser(opts, parsers) { locStart, locEnd, }; - } catch (err) { + } catch { /* istanbul ignore next */ throw new ConfigError(`Couldn't resolve parser "${opts.parser}"`); } diff --git a/src/main/range-util.js b/src/main/range-util.js index 20860a0a1..718026a55 100644 --- a/src/main/range-util.js +++ b/src/main/range-util.js @@ -128,7 +128,7 @@ const graphqlSourceElements = new Set([ ]); function isSourceElement(opts, node) { /* istanbul ignore next */ - if (node == null) { + if (!node) { return false; } switch (opts.parser) { diff --git a/src/main/support.js b/src/main/support.js index 8bff0cf03..613403152 100644 --- a/src/main/support.js +++ b/src/main/support.js @@ -1,5 +1,6 @@ "use strict"; +const fromPairs = require("lodash/fromPairs"); const semver = { compare: require("semver/functions/compare"), lt: require("semver/functions/lt"), @@ -30,7 +31,7 @@ function getSupportInfo({ const version = currentVersion.split("-", 1)[0]; const languages = plugins - .reduce((all, plugin) => all.concat(plugin.languages || []), []) + .reduce((all, plugin) => [...all, ...(plugin.languages || [])], []) .filter(filterSince); const options = arrayify( @@ -64,16 +65,15 @@ function getSupportInfo({ } } - const pluginDefaults = plugins - .filter( - (plugin) => - plugin.defaultOptions && - plugin.defaultOptions[option.name] !== undefined - ) - .reduce((reduced, plugin) => { - reduced[plugin.name] = plugin.defaultOptions[option.name]; - return reduced; - }, {}); + const pluginDefaults = fromPairs( + plugins + .filter( + (plugin) => + plugin.defaultOptions && + plugin.defaultOptions[option.name] !== undefined + ) + .map((plugin) => [plugin.name, plugin.defaultOptions[option.name]]) + ); return { ...option, pluginDefaults }; }); diff --git a/src/utils/front-matter.js b/src/utils/front-matter.js deleted file mode 100644 index b24e86221..000000000 --- a/src/utils/front-matter.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; - -const escape = require("escape-string-regexp"); -const { - builders: { hardline, markAsRoot }, -} = require("../document"); - -const DELIMITER_MAP = { - "---": "yaml", - "+++": "toml", -}; - -function parse(text) { - const delimiterRegex = Object.keys(DELIMITER_MAP).map(escape).join("|"); - - const match = text.match( - // trailing spaces after delimiters are allowed - new RegExp( - `^(${delimiterRegex})([^\\n]*)\\n(?:|([\\s\\S]*?)\\n)\\1[^\\n\\S]*(\\n|$)` - ) - ); - - if (match === null) { - return { frontMatter: null, content: text }; - } - - const [raw, delimiter, language, value = ""] = match; - let lang = DELIMITER_MAP[delimiter]; - if (lang !== "toml" && language && language.trim()) { - lang = language.trim(); - } - - return { - frontMatter: { - type: "front-matter", - lang, - value, - raw: raw.replace(/\n$/, ""), - }, - content: raw.replace(/[^\n]/g, " ") + text.slice(raw.length), - }; -} - -function print(node, textToDoc) { - if (node.lang === "yaml") { - const value = node.value.trim(); - const doc = value - ? textToDoc(value, { parser: "yaml" }, { stripTrailingHardline: true }) - : ""; - return markAsRoot(["---", hardline, doc, doc ? hardline : "", "---"]); - } -} - -module.exports = { parse, print }; diff --git a/src/utils/front-matter/parse.js b/src/utils/front-matter/parse.js new file mode 100644 index 000000000..5399142fc --- /dev/null +++ b/src/utils/front-matter/parse.js @@ -0,0 +1,49 @@ +"use strict"; + +const frontMatterRegex = new RegExp( + "^(?-{3}|\\+{3})" + + // trailing spaces after delimiters are allowed + "(?[^\\n]*)" + + "\\n(?:|(?[\\S\\s]*?)\\n)" + + // In some markdown processors such as pandoc, + // "..." can be used as the end delimiter for YAML front-matter. + // Adding `\.{3}` make the regex matches `+++\n...`, but we'll exclude it later + "(?\\k|\\.{3})" + + "[^\\S\\n]*(?:\\n|$)" +); + +function parse(text) { + const match = text.match(frontMatterRegex); + if (!match) { + return { content: text }; + } + + const { startDelimiter, language, value = "", endDelimiter } = match.groups; + + let lang = language.trim() || "yaml"; + if (startDelimiter === "+++") { + lang = "toml"; + } + + // Only allow yaml to parse with a different end delimiter + if (lang !== "yaml" && startDelimiter !== endDelimiter) { + return { content: text }; + } + + const [raw] = match; + const frontMatter = { + type: "front-matter", + lang, + value, + startDelimiter, + endDelimiter, + raw: raw.replace(/\n$/, ""), + }; + + return { + frontMatter, + content: raw.replace(/[^\n]/g, " ") + text.slice(raw.length), + }; +} + +module.exports = parse; diff --git a/src/utils/front-matter/print.js b/src/utils/front-matter/print.js new file mode 100644 index 000000000..d28089735 --- /dev/null +++ b/src/utils/front-matter/print.js @@ -0,0 +1,23 @@ +"use strict"; + +const { + builders: { hardline, markAsRoot }, +} = require("../../document"); + +function print(node, textToDoc) { + if (node.lang === "yaml") { + const value = node.value.trim(); + const doc = value + ? textToDoc(value, { parser: "yaml" }, { stripTrailingHardline: true }) + : ""; + return markAsRoot([ + node.startDelimiter, + hardline, + doc, + doc ? hardline : "", + node.endDelimiter, + ]); + } +} + +module.exports = print; diff --git a/tests/angular/angular/__snapshots__/jsfmt.spec.js.snap b/tests/angular/angular/__snapshots__/jsfmt.spec.js.snap index 437a6d4fb..0f0b5368a 100644 --- a/tests/angular/angular/__snapshots__/jsfmt.spec.js.snap +++ b/tests/angular/angular/__snapshots__/jsfmt.spec.js.snap @@ -1143,7 +1143,8 @@ printWidth: 1 b() " (event)=" - foo = $event + foo = + $event " (event)=" (foo == @@ -8343,7 +8344,8 @@ can be found in the LICENSE file at http://angular.io/license >
@@ -8363,7 +8365,8 @@ can be found in the LICENSE file at http://angular.io/license >
@@ -8982,7 +8985,8 @@ can be found in the LICENSE file at http://angular.io/license
@@ -9136,7 +9140,8 @@ can be found in the LICENSE file at http://angular.io/license fontSizePx " (sizeChange)=" - fontSizePx = $event + fontSizePx = + $event " >
@@ -9198,7 +9203,8 @@ bindon-ngModel currentHero.name " (ngModelChange)=" - currentHero.name = $event + currentHero.name = + $event " /> (ngModelChange)="...name=$event" @@ -9274,7 +9280,8 @@ bindon-ngModel !isUnchanged " (change)=" - isUnchanged = !isUnchanged + isUnchanged = + !isUnchanged " /> | @@ -10203,7 +10210,8 @@ bindon-ngModel
diff --git a/tests/css/range/__snapshots__/jsfmt.spec.js.snap b/tests/css/range/__snapshots__/jsfmt.spec.js.snap index 090dfdf59..887c9437f 100644 --- a/tests/css/range/__snapshots__/jsfmt.spec.js.snap +++ b/tests/css/range/__snapshots__/jsfmt.spec.js.snap @@ -9,7 +9,7 @@ rangeEnd: 4 =====================================input====================================== > 1 | .x{} | ^^^^ - 2 | + 2 | =====================================output===================================== .x{} diff --git a/tests/flow-repo/declare_export/__snapshots__/jsfmt.spec.js.snap b/tests/flow-repo/declare_export/__snapshots__/jsfmt.spec.js.snap index aa8ae112a..dac461c32 100644 --- a/tests/flow-repo/declare_export/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow-repo/declare_export/__snapshots__/jsfmt.spec.js.snap @@ -1327,8 +1327,8 @@ var numberValue6 = require("ES6_ExportFrom_Intermediary2").numberValue1; var ap1: number = numberValue6; var ap2: string = numberValue6; // Error: number ~> string -var numberValue2_renamed2 = require("ES6_ExportFrom_Intermediary2") - .numberValue2_renamed2; +var numberValue2_renamed2 = + require("ES6_ExportFrom_Intermediary2").numberValue2_renamed2; var aq1: number = numberValue2_renamed2; var aq2: string = numberValue2_renamed2; // Error: number ~> string diff --git a/tests/flow-repo/dictionary/__snapshots__/jsfmt.spec.js.snap b/tests/flow-repo/dictionary/__snapshots__/jsfmt.spec.js.snap index 4e23246fb..6d15b8a65 100644 --- a/tests/flow-repo/dictionary/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow-repo/dictionary/__snapshots__/jsfmt.spec.js.snap @@ -56,9 +56,10 @@ function foo0( return x; } -function foo2(x: { +function foo2(x: { [key: string]: number }): { [key: string]: number, -}): { [key: string]: number, +toString: () => string } { + +toString: () => string, +} { // x's prototype has a toString method return x; } @@ -468,9 +469,10 @@ function mix_with_declared_props(o: { [k: number]: X, p: Y }, x: X, y: Y) { } // Indeed, dict types are still Objects and have Object.prototype stuff -function object_prototype(o: { +function object_prototype(o: { [k: string]: number }): { [k: string]: number, -}): { [k: string]: number, +toString: () => string } { + +toString: () => string, +} { (o.toString(): boolean); // error: string ~> boolean return o; // ok } @@ -776,9 +778,10 @@ function foo3(x: { [key: string]: number }): { foo: number } { } // error: foo can't exist in x -function foo4(x: { +function foo4(x: { [key: string]: number }): { [key: string]: number, -}): { [key: string]: number, foo: string } { + foo: string, +} { return x; } diff --git a/tests/flow-repo/es6modules/__snapshots__/jsfmt.spec.js.snap b/tests/flow-repo/es6modules/__snapshots__/jsfmt.spec.js.snap index c3e3c56e7..2e4d8d1ac 100644 --- a/tests/flow-repo/es6modules/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow-repo/es6modules/__snapshots__/jsfmt.spec.js.snap @@ -1480,8 +1480,8 @@ var numberValue6 = require("ES6_ExportFrom_Intermediary2").numberValue1; var ap1: number = numberValue6; var ap2: string = numberValue6; // Error: number ~> string -var numberValue2_renamed2 = require("ES6_ExportFrom_Intermediary2") - .numberValue2_renamed2; +var numberValue2_renamed2 = + require("ES6_ExportFrom_Intermediary2").numberValue2_renamed2; var aq1: number = numberValue2_renamed2; var aq2: string = numberValue2_renamed2; // Error: number ~> string diff --git a/tests/flow-repo/missing_annotation/__snapshots__/jsfmt.spec.js.snap b/tests/flow-repo/missing_annotation/__snapshots__/jsfmt.spec.js.snap index 5a14b8fc2..5637cc40b 100644 --- a/tests/flow-repo/missing_annotation/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow-repo/missing_annotation/__snapshots__/jsfmt.spec.js.snap @@ -132,9 +132,7 @@ var Foo = { }; }, - d: function ( - arg: string - ): { + d: function (arg: string): { bar: string, } { return { diff --git a/tests/flow-repo/spread/__snapshots__/jsfmt.spec.js.snap b/tests/flow-repo/spread/__snapshots__/jsfmt.spec.js.snap index 81b05c5d0..f064aed9a 100644 --- a/tests/flow-repo/spread/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow-repo/spread/__snapshots__/jsfmt.spec.js.snap @@ -46,9 +46,7 @@ function parseCounter(line: string): number { return 0; } -function parseGroup( - lines: Array -): { +function parseGroup(lines: Array): { counter: number, begin: number, end: number, diff --git a/tests/flow/await/__snapshots__/jsfmt.spec.js.snap b/tests/flow/await/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..7b5c5e513 --- /dev/null +++ b/tests/flow/await/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,85 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`await-keywords.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +export class C1 { + p = await (0); +} + +await (0); + +async function foo() { + function bar(x = await (2)) {} +} + +async (x = await (2)) => {}; + +export class C2 { + p = await 0; +} + +function foo(promise) { await (promise); } + +function a() { + return await (1) +} + +() => { await (x) }; + +function foo() { + await + (foo); +} + +export class C { + p = await (0); +} + +await (0); + +=====================================output===================================== +export class C1 { + p = await(0); +} + +await(0); + +async function foo() { + function bar(x = await(2)) {} +} + +async (x = await(2)) => {}; + +export class C2 { + p = await; + 0; +} + +function foo(promise) { + await(promise); +} + +function a() { + return await(1); +} + +() => { + await(x); +}; + +function foo() { + await(foo); +} + +export class C { + p = await(0); +} + +await(0); + +================================================================================ +`; diff --git a/tests/flow/await/await-keywords.js b/tests/flow/await/await-keywords.js new file mode 100644 index 000000000..7cf399e3b --- /dev/null +++ b/tests/flow/await/await-keywords.js @@ -0,0 +1,34 @@ +export class C1 { + p = await (0); +} + +await (0); + +async function foo() { + function bar(x = await (2)) {} +} + +async (x = await (2)) => {}; + +export class C2 { + p = await 0; +} + +function foo(promise) { await (promise); } + +function a() { + return await (1) +} + +() => { await (x) }; + +function foo() { + await + (foo); +} + +export class C { + p = await (0); +} + +await (0); diff --git a/tests/flow/await/jsfmt.spec.js b/tests/flow/await/jsfmt.spec.js new file mode 100644 index 000000000..b9a908981 --- /dev/null +++ b/tests/flow/await/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, ["flow"]); diff --git a/tests/flow/declare-function/__snapshots__/jsfmt.spec.js.snap b/tests/flow/declare-function/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..da449cf2a --- /dev/null +++ b/tests/flow/declare-function/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`export.js format 1`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 + | printWidth +=====================================input====================================== +declare function foo(): void; +export { foo }; + +=====================================output===================================== +declare function foo(): void; +export { foo }; + +================================================================================ +`; diff --git a/tests/flow/declare-function/export.js b/tests/flow/declare-function/export.js new file mode 100644 index 000000000..05b292825 --- /dev/null +++ b/tests/flow/declare-function/export.js @@ -0,0 +1,2 @@ +declare function foo(): void; +export { foo }; diff --git a/tests/flow/declare-function/jsfmt.spec.js b/tests/flow/declare-function/jsfmt.spec.js new file mode 100644 index 000000000..fbfa6501a --- /dev/null +++ b/tests/flow/declare-function/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, ["flow", "babel"]); diff --git a/tests/flow/function-type-param/__snapshots__/jsfmt.spec.js.snap b/tests/flow/function-type-param/__snapshots__/jsfmt.spec.js.snap index ed5860f77..98292bdd1 100644 --- a/tests/flow/function-type-param/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/function-type-param/__snapshots__/jsfmt.spec.js.snap @@ -17,16 +17,14 @@ export const testFunctionOnOptionsAsArgument: (?a, ((?a) => T1)) => T1 = f =====================================output===================================== let f: (((?A) => B)) => B; -export const testFunctionOnOptionsAsArgument: ( - ?a, - ((?a) => T1) -) => T1 = function _(Arg1, Arg2) { - const result = TypesBS.testFunctionOnOptionsAsArgument( - Arg1 == null ? undefined : Arg1, - Arg2 - ); - return result; -}; +export const testFunctionOnOptionsAsArgument: (?a, ((?a) => T1)) => T1 = + function _(Arg1, Arg2) { + const result = TypesBS.testFunctionOnOptionsAsArgument( + Arg1 == null ? undefined : Arg1, + Arg2 + ); + return result; + }; ================================================================================ `; diff --git a/tests/flow/generic/__snapshots__/jsfmt.spec.js.snap b/tests/flow/generic/__snapshots__/jsfmt.spec.js.snap index efa8b4ae1..9a067af76 100644 --- a/tests/flow/generic/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/generic/__snapshots__/jsfmt.spec.js.snap @@ -243,7 +243,8 @@ trailingComma: "all" const longVariableName: Array = this.foo.bar.baz.collider.body.vertices.reduce(); =====================================output===================================== -const longVariableName: Array = this.foo.bar.baz.collider.body.vertices.reduce(); +const longVariableName: Array = + this.foo.bar.baz.collider.body.vertices.reduce(); ================================================================================ `; @@ -257,7 +258,8 @@ printWidth: 80 const longVariableName: Array = this.foo.bar.baz.collider.body.vertices.reduce(); =====================================output===================================== -const longVariableName: Array = this.foo.bar.baz.collider.body.vertices.reduce(); +const longVariableName: Array = + this.foo.bar.baz.collider.body.vertices.reduce(); ================================================================================ `; diff --git a/tests/flow/method/__snapshots__/jsfmt.spec.js.snap b/tests/flow/method/__snapshots__/jsfmt.spec.js.snap index fc9bddf33..b67945453 100644 --- a/tests/flow/method/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/method/__snapshots__/jsfmt.spec.js.snap @@ -24,6 +24,63 @@ type Foo = { ================================================================================ `; +exports[`consistent-breaking.js format 1`] = ` +====================================options===================================== +parsers: ["flow", "babel"] +printWidth: 80 + | printWidth +=====================================input====================================== +class Foo1 { + flowParseFunctionTypeParams( + params: N.FlowFunctionTypeParam[] = [], + ): { params: N.FlowFunctionTypeParam[], rest: ?N.FlowFunctionTypeParam } { + // ... + } +} + +type Foo2 = { + flowParseFunctionTypeParams( + params: N.FlowFunctionTypeParam[] + ): { params: N.FlowFunctionTypeParam[], rest: ?N.FlowFunctionTypeParam } +} + +{ + function flowParseFunctionTypeParams( + params: N.FlowFunctionTypeParam[] + ): { params: N.FlowFunctionTypeParam[], rest: ?N.FlowFunctionTypeParam } { + // ... + } +} + +=====================================output===================================== +class Foo1 { + flowParseFunctionTypeParams(params: N.FlowFunctionTypeParam[] = []): { + params: N.FlowFunctionTypeParam[], + rest: ?N.FlowFunctionTypeParam, + } { + // ... + } +} + +type Foo2 = { + flowParseFunctionTypeParams(params: N.FlowFunctionTypeParam[]): { + params: N.FlowFunctionTypeParam[], + rest: ?N.FlowFunctionTypeParam, + }, +}; + +{ + function flowParseFunctionTypeParams(params: N.FlowFunctionTypeParam[]): { + params: N.FlowFunctionTypeParam[], + rest: ?N.FlowFunctionTypeParam, + } { + // ... + } +} + +================================================================================ +`; + exports[`method.js format 1`] = ` ====================================options===================================== parsers: ["flow", "babel"] diff --git a/tests/flow/method/consistent-breaking.js b/tests/flow/method/consistent-breaking.js new file mode 100644 index 000000000..d5747c05f --- /dev/null +++ b/tests/flow/method/consistent-breaking.js @@ -0,0 +1,21 @@ +class Foo1 { + flowParseFunctionTypeParams( + params: N.FlowFunctionTypeParam[] = [], + ): { params: N.FlowFunctionTypeParam[], rest: ?N.FlowFunctionTypeParam } { + // ... + } +} + +type Foo2 = { + flowParseFunctionTypeParams( + params: N.FlowFunctionTypeParam[] + ): { params: N.FlowFunctionTypeParam[], rest: ?N.FlowFunctionTypeParam } +} + +{ + function flowParseFunctionTypeParams( + params: N.FlowFunctionTypeParam[] + ): { params: N.FlowFunctionTypeParam[], rest: ?N.FlowFunctionTypeParam } { + // ... + } +} diff --git a/tests/flow/parameter-with-type/__snapshots__/jsfmt.spec.js.snap b/tests/flow/parameter-with-type/__snapshots__/jsfmt.spec.js.snap index 17c451403..7c70b59df 100644 --- a/tests/flow/parameter-with-type/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/parameter-with-type/__snapshots__/jsfmt.spec.js.snap @@ -11,7 +11,8 @@ const f = ({}: MyVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongTyp function g({}: Foo) {} =====================================output===================================== -const f = ({}: MyVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongType) => {}; +const f = + ({}: MyVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongType) => {}; function g({}: Foo) {} ================================================================================ @@ -27,7 +28,8 @@ const f = ({}: MyVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongTyp function g({}: Foo) {} =====================================output===================================== -const f = ({}: MyVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongType) => {}; +const f = + ({}: MyVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongType) => {}; function g({}: Foo) {} ================================================================================ diff --git a/tests/flow/this-annotation/__snapshots__/jsfmt.spec.js.snap b/tests/flow/this-annotation/__snapshots__/jsfmt.spec.js.snap index 676102b83..12da03297 100644 --- a/tests/flow/this-annotation/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/this-annotation/__snapshots__/jsfmt.spec.js.snap @@ -54,17 +54,6 @@ function baz(this: number, ...a) {} ================================================================================ `; -exports[`function_type.js - {"trailingComma":"all"} [babel-flow] format 1`] = ` -"Unexpected token, expected \\")\\" (3:16) - 1 | // @flow - 2 | -> 3 | type T = (this : number, a : string, b : number) => void - | ^ - 4 | - 5 | type U = (this : number, ...c : any) => void - 6 | " -`; - exports[`function_type.js - {"trailingComma":"all"} format 1`] = ` ====================================options===================================== parsers: ["flow", "babel-flow"] @@ -92,17 +81,6 @@ type V = (this: number) => void; ================================================================================ `; -exports[`line_break.js - {"trailingComma":"all"} [babel-flow] format 1`] = ` -"Unexpected token, expected \\")\\" (3:15) - 1 | // @flow - 2 | -> 3 | type T = (this: boolean, - | ^ - 4 | a: number, - 5 | - 6 | b: number," -`; - exports[`line_break.js - {"trailingComma":"all"} format 1`] = ` ====================================options===================================== parsers: ["flow", "babel-flow"] @@ -195,17 +173,6 @@ class A { ================================================================================ `; -exports[`union_type.js - {"trailingComma":"all"} [babel-flow] format 1`] = ` -"Unexpected token, expected \\")\\" (4:7) - 2 | - 3 | type A = ( -> 4 | this: | SupperLongLongLongLongLongLongLongLongLongLongLongType | FooBarBazLorem12345, - | ^ - 5 | b: number, - 6 | ) => boolean; - 7 | " -`; - exports[`union_type.js - {"trailingComma":"all"} format 1`] = ` ====================================options===================================== parsers: ["flow", "babel-flow"] diff --git a/tests/flow/this-annotation/jsfmt.spec.js b/tests/flow/this-annotation/jsfmt.spec.js index 4d93126a0..eaab82f9e 100644 --- a/tests/flow/this-annotation/jsfmt.spec.js +++ b/tests/flow/this-annotation/jsfmt.spec.js @@ -1,6 +1,3 @@ run_spec(__dirname, ["flow", "babel-flow"], { trailingComma: "all", - errors: { - "babel-flow": ["function_type.js", "line_break.js", "union_type.js"], - }, }); diff --git a/tests/flow/type-declarations/__snapshots__/jsfmt.spec.js.snap b/tests/flow/type-declarations/__snapshots__/jsfmt.spec.js.snap index 04af9fe80..10b6fff0c 100644 --- a/tests/flow/type-declarations/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/type-declarations/__snapshots__/jsfmt.spec.js.snap @@ -107,11 +107,14 @@ type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey2 = Som export type AdamPlacementValidationSingleErrorKey = "SOME_FANCY_TARGETS.GLOBAL_TARGET" -export type SomeReallyLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = true +export type SomeReallyLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = + true -export type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = null +export type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = + null -type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey2 = SomeReallyLongLongLongLongLongLongIdentifier +type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey2 = + SomeReallyLongLongLongLongLongLongIdentifier ================================================================================ `; @@ -135,11 +138,14 @@ type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey2 = Som export type AdamPlacementValidationSingleErrorKey = "SOME_FANCY_TARGETS.GLOBAL_TARGET"; -export type SomeReallyLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = true; +export type SomeReallyLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = + true; -export type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = null; +export type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey = + null; -type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey2 = SomeReallyLongLongLongLongLongLongIdentifier; +type SomeOtherLongLongLongLongLongLongLongLongLongLongLongLongLongLongKey2 = + SomeReallyLongLongLongLongLongLongIdentifier; ================================================================================ `; diff --git a/tests/flow/type-parameters/__snapshots__/jsfmt.spec.js.snap b/tests/flow/type-parameters/__snapshots__/jsfmt.spec.js.snap index 4f8c9b141..59bfe664d 100644 --- a/tests/flow/type-parameters/__snapshots__/jsfmt.spec.js.snap +++ b/tests/flow/type-parameters/__snapshots__/jsfmt.spec.js.snap @@ -12,7 +12,8 @@ class X { =====================================output===================================== class X { - a: B<> = SuperSuperSuperSuperSuperSuperSuperSuperSuperSuperSuperSuperSuperLong; + a: B<> = + SuperSuperSuperSuperSuperSuperSuperSuperSuperSuperSuperSuperSuperLong; } ================================================================================ @@ -30,10 +31,14 @@ const foo3: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo const foo4: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<0xDeeD_BeeFn> = a; =====================================output===================================== -const foo1: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo = a; -const foo2: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<1n> = a; -const foo3: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<1_1n> = a; -const foo4: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<0xdeed_beefn> = a; +const foo1: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo = + a; +const foo2: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<1n> = + a; +const foo3: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<1_1n> = + a; +const foo4: Fooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo<0xdeed_beefn> = + a; ================================================================================ `; diff --git a/tests/graphql/range/__snapshots__/jsfmt.spec.js.snap b/tests/graphql/range/__snapshots__/jsfmt.spec.js.snap index 648665abd..7a5bd1d3d 100644 --- a/tests/graphql/range/__snapshots__/jsfmt.spec.js.snap +++ b/tests/graphql/range/__snapshots__/jsfmt.spec.js.snap @@ -9,7 +9,7 @@ rangeEnd: 3 =====================================input====================================== > 1 | {NPC{life}} | ^^^ - 2 | + 2 | =====================================output===================================== { NPC { diff --git a/tests/handlebars/attr-node/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/attr-node/__snapshots__/jsfmt.spec.js.snap index f5438b7a5..525b90080 100644 --- a/tests/handlebars/attr-node/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/attr-node/__snapshots__/jsfmt.spec.js.snap @@ -41,21 +41,26 @@ printWidth: 80 '>
=====================================output===================================== -
+ newlines" +>
-
+ newlines" +> -
+" +> -
+" +> ================================================================================ `; @@ -108,38 +116,32 @@ weirdly formatted {{this.classes}}">String

+ + =====================================output=====================================
-

String

+

String

+

String

- -weirdly formatted - - -classes"> - String -

-

- String -

-

- String -

+ +================================================================================ +`; + +exports[`quotes.hbs format 1`] = ` +====================================options===================================== +parsers: ["glimmer"] +printWidth: 80 + | printWidth +=====================================input====================================== +
+
+ +=====================================output===================================== +
+
================================================================================ `; diff --git a/tests/handlebars/attr-node/class-name.hbs b/tests/handlebars/attr-node/class-name.hbs index c14f0fbe9..93d5a2b96 100644 --- a/tests/handlebars/attr-node/class-name.hbs +++ b/tests/handlebars/attr-node/class-name.hbs @@ -22,3 +22,6 @@ weirdly formatted {{this.classes}}">String

+ + diff --git a/tests/handlebars/attr-node/quotes.hbs b/tests/handlebars/attr-node/quotes.hbs new file mode 100644 index 000000000..75726267b --- /dev/null +++ b/tests/handlebars/attr-node/quotes.hbs @@ -0,0 +1,2 @@ +
+
diff --git a/tests/handlebars/basics/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/basics/__snapshots__/jsfmt.spec.js.snap index 56317cf5a..5cad2bbce 100644 --- a/tests/handlebars/basics/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/basics/__snapshots__/jsfmt.spec.js.snap @@ -37,7 +37,8 @@ printWidth: 80 =====================================output===================================== -{{@greeting}}, {{@name}}! +{{@greeting}}, +{{@name}}!
- - hello - + hello - + @@ -64,12 +61,9 @@ printWidth: 80 -
- Hello -
+
Hello
-
- Link - +>Link @@ -165,23 +168,20 @@ printWidth: 80 =====================================output=====================================
- hey -
+ class="a very long list of classes that exceeds + {{eighty}} + chars with emtpy spaces" +>hey - Link - +>Link diff --git a/tests/handlebars/element-node/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/element-node/__snapshots__/jsfmt.spec.js.snap index a4ede7862..7d36706bc 100644 --- a/tests/handlebars/element-node/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/element-node/__snapshots__/jsfmt.spec.js.snap @@ -67,7 +67,8 @@ printWidth: 80
- A long enough string to trigger a line break that would prevent wrapping more and more. + A long enough string to trigger a line break that would prevent wrapping more + and more.
diff --git a/tests/handlebars/escape/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/escape/__snapshots__/jsfmt.spec.js.snap index 63fa6f626..c300b89ae 100644 --- a/tests/handlebars/escape/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/escape/__snapshots__/jsfmt.spec.js.snap @@ -8,8 +8,21 @@ printWidth: 80 =====================================input======================================

Some escaped characters: < > &

=====================================output===================================== -

- Some escaped characters: < > & -

+

Some escaped characters: < > &

+================================================================================ +`; + +exports[`mustache.hbs format 1`] = ` +====================================options===================================== +parsers: ["glimmer"] +printWidth: 80 + | printWidth +=====================================input====================================== +
\\{{
+
\\{{
+ +=====================================output===================================== +
\\{{
+
\\{{
================================================================================ `; diff --git a/tests/handlebars/escape/mustache.hbs b/tests/handlebars/escape/mustache.hbs new file mode 100644 index 000000000..96e7a2f8c --- /dev/null +++ b/tests/handlebars/escape/mustache.hbs @@ -0,0 +1,2 @@ +
\{{
+
\{{
diff --git a/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap index 5cb12c015..278e25683 100644 --- a/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/html-whitespace-sensitivity/__snapshots__/jsfmt.spec.js.snap @@ -126,9 +126,8 @@ sometimes{{nogaps}}areimportant =====================================output=====================================

- Hi {{firstName}} {{ - lastName - }} , welcome! + Hi {{firstName}} {{lastName}} , + welcome!

{{#component propA}} for {{propB}} do {{propC}} f @@ -152,25 +151,31 @@ sometimes{{nogaps}}areimportant hey
@@ -178,10 +183,8 @@ hey {{name}} -Some sentence with {{dynamic -}} expressions. - -sometimes{{nogaps +Some sentence with {{dynamic}} +expressions. sometimes{{nogaps }}areimportant @@ -459,21 +462,27 @@ sometimes{{nogaps}}areimportant hey - +
- +
- - + +
@@ -482,9 +491,7 @@ hey Some sentence with {{dynamic}} -expressions. - -sometimes{{nogaps +expressions. sometimes{{nogaps }}areimportant {{name}} @@ -580,7 +587,6 @@ is your name - ================================================================================ `; @@ -632,7 +638,7 @@ printWidth: 40 =====================================input======================================
   
=====================================output===================================== -
   
+
   
================================================================================ `; @@ -684,7 +690,7 @@ printWidth: 40 =====================================input======================================     =====================================output===================================== -     +     ================================================================================ `; diff --git a/tests/handlebars/mustache-statement/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/mustache-statement/__snapshots__/jsfmt.spec.js.snap index 7890ecc17..8ef3442e7 100644 --- a/tests/handlebars/mustache-statement/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/mustache-statement/__snapshots__/jsfmt.spec.js.snap @@ -99,41 +99,37 @@ singleQuote: true />
@@ -266,41 +262,37 @@ printWidth: 80 />
@@ -504,11 +496,9 @@ another non-escaped mustache: =====================================output===================================== an escaped mustache: -\\{{my-component}} -a non-escaped mustache: +\\{{my-component}} a non-escaped mustache: \\\\{{my-component}} -another non-escaped mustache: -\\\\\\{{my-component}} +another non-escaped mustache: \\\\\\{{my-component}} ================================================================================ `; @@ -527,10 +517,8 @@ another non-escaped mustache: =====================================output===================================== an escaped mustache: -\\{{my-component}} -a non-escaped mustache: +\\{{my-component}} a non-escaped mustache: \\\\{{my-component}} -another non-escaped mustache: -\\\\\\{{my-component}} +another non-escaped mustache: \\\\\\{{my-component}} ================================================================================ `; diff --git a/tests/handlebars/newline/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/newline/__snapshots__/jsfmt.spec.js.snap index afb2f0335..746b4d81b 100644 --- a/tests/handlebars/newline/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/newline/__snapshots__/jsfmt.spec.js.snap @@ -79,56 +79,38 @@ printWidth: 80 =====================================output=====================================

Welcome to the - - Ember.js Guides - - . - This documentation will take you from total beginner to Ember expert. + Ember.js Guides. This documentation will take you from total + beginner to Ember expert.

{{! newlines text }}
- hi - there - how - - are you - - - are you fine today? + hi there how are you are you fine today?
{{! newlines text spaced }}
- space above - space below + space above space below +
{{! newlines elems spaced }}
- - space above - - - space below - + space above + + space below +
{{! newlines mixed }}
hi - - there - + there - how - - are - - you - + how are + you are you fine today?
@@ -136,7 +118,9 @@ printWidth: 80 {{! newlines elems }}
+
+
hi @@ -144,6 +128,7 @@ printWidth: 80
+
================================================================================ `; diff --git a/tests/handlebars/prettier-ignore/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/prettier-ignore/__snapshots__/jsfmt.spec.js.snap index 16840c8e8..de2480488 100644 --- a/tests/handlebars/prettier-ignore/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/prettier-ignore/__snapshots__/jsfmt.spec.js.snap @@ -66,11 +66,13 @@ printWidth: 80
{{#another-sibling as |isFixed|}} -
- {{isFixed}} + +
{{isFixed}} All of this should be fixed +
+ {{/another-sibling}}
================================================================================ diff --git a/tests/handlebars/single-quote-option/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/single-quote-option/__snapshots__/jsfmt.spec.js.snap index 379c2bf64..891b513bc 100644 --- a/tests/handlebars/single-quote-option/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/single-quote-option/__snapshots__/jsfmt.spec.js.snap @@ -98,24 +98,20 @@ singleQuote: false ================================================================================ `; @@ -150,24 +146,20 @@ singleQuote: true ================================================================================ `; diff --git a/tests/handlebars/text-wrap/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/text-wrap/__snapshots__/jsfmt.spec.js.snap index 6b196b202..7f322201e 100644 --- a/tests/handlebars/text-wrap/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/text-wrap/__snapshots__/jsfmt.spec.js.snap @@ -58,7 +58,7 @@ printWidth: 80
- First
+ First
Second
Third
@@ -155,75 +155,35 @@ printWidth: 80 =====================================output===================================== {{! Wrapping text }}
- Some text that would need to wrap on to a new line in order to display correctly and nicely + Some text that would need to wrap on to a new line in order to display + correctly and nicely
{{! Wrapping tags }}
- - f - - - - f - - - - f - - - - f - - - - f - - - - f - + f + f + f + f + f + f
{{! Wrapping tags }}
- - f - - - f - - - f - - - f - - - f - - - f - + ffffff
{{! Wrapping tags }}
- - aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa - - - - f - + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + f
- before -
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at mollis lorem. -
- after + before
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur + at mollis lorem.
after
@@ -231,24 +191,32 @@ printWidth: 80
- before {{stuff}} after {{stuff}} after {{stuff}} after {{stuff}} after {{ - stuff - }} after {{stuff}} {{stuff}} {{stuff}} after {{stuff}} after + before + {{stuff}} + after + {{stuff}} + after + {{stuff}} + after + {{stuff}} + after + {{stuff}} + after + {{stuff}} + {{stuff}} + {{stuff}} + after + {{stuff}} + after
Please state your - - name - + name and - - occupation - + occupation for the board of - - school - + school directors.
@@ -256,9 +224,7 @@ printWidth: 80 First second third
- Something -
+ >Something
@@ -272,8 +238,7 @@ printWidth: 80
- First -
+ First
Second
Third @@ -281,42 +246,33 @@ printWidth: 80 {{! leading whitespace }}
- First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth Eleventh Twelfth Thirteenth Fourteenth -
+ First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth Eleventh + Twelfth Thirteenth Fourteenth
{{! trailing whitespace }} -
- First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth Eleventh Twelfth Thirteenth Fourteenth +
First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth Eleventh + Twelfth Thirteenth Fourteenth
{{! no leading or trailing whitespace }} -
- First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth Eleventh Twelfth Thirteenth Fourteenth -
+
First Second Third Fourth Fifth Sixth Seventh Eighth Ninth Tenth Eleventh + Twelfth Thirteenth Fourteenth
{{! translation leave text around tag }}
First - - , - ( - + , ( Second - - ) + )
First second third fourth fifth sixth seventh - - , - ( - + , ( Second - - ) + )
{{! this really should split across lines }} @@ -336,56 +292,45 @@ printWidth: 80 fourth="fourth" fifth="fifth" sixth="sixth" -> -
+>
{{! whitespace on newline }}
First
-
Second
-
Third
{{! around multiline element }} -
- Before -
- {"Enough text to make this element wrap on to multiple lines when formatting"} -
- After -
+
Before +
{"Enough text to make this element wrap on to multiple lines when + formatting"}
+ After
{{! around multiline element second pass }}
Before{" "}
- { - "Enough text to make this element wrap on to multiple lines when formatting" - } -
- {" "} - After + { "Enough text to make this element wrap on to multiple lines when + formatting" } +
{" "} After {{! dont preserve blank lines when contains text }}
-
- Zeroth -
-
- First -
+
Zeroth
+ +
First
Second +
{{! multiple expressions }} @@ -398,20 +343,14 @@ printWidth: 80 {{! single expression child tags }}
You currently have - - {{dashboardStr}} - + {{dashboardStr}} and - - {{userStr}} - + {{userStr}}
{{! expression does not break }} -
- texty text text text text text text text text text text text {{ - this.props.type - }} +
texty text text text text text text text text text text text + {{this.props.type}}
// FIXME diff --git a/tests/handlebars/text-wrap/test.hbs b/tests/handlebars/text-wrap/test.hbs index 449053348..c153f5ba5 100644 --- a/tests/handlebars/text-wrap/test.hbs +++ b/tests/handlebars/text-wrap/test.hbs @@ -50,7 +50,7 @@
- First
+ First
Second
Third
diff --git a/tests/handlebars/whitespace/__snapshots__/jsfmt.spec.js.snap b/tests/handlebars/whitespace/__snapshots__/jsfmt.spec.js.snap index a8fdb2834..fbd622509 100644 --- a/tests/handlebars/whitespace/__snapshots__/jsfmt.spec.js.snap +++ b/tests/handlebars/whitespace/__snapshots__/jsfmt.spec.js.snap @@ -37,60 +37,35 @@ printWidth: 40 {{! after }} foo - - bar - + bar {{! before }} - - bar - + bar foo {{! within }} foo - - bar - + bar {{! break components }}
-

- foo - - bar bar bar - -

-

- - - yep - - -

+

foobar bar bar

yep

-

- nope -

+

nope

hello - - hi - - - - sdkflsdfjk - -
-; + hi + sdkflsdfjk +
; ================================================================================ `; @@ -117,29 +92,29 @@ printWidth: 40 hey =====================================output===================================== -

- Hi {{firstName}} {{ - lastName - }} , welcome! -

+

Hi + {{firstName}} + {{lastName}} + , welcome!

{{#component propA}} - for {{propB}} do {{propC}} f + for + {{propB}} + do + {{propC}} + f {{/component}} {{#component propA}} - for {{propB}} - - name - - do {{propC}} f + for + {{propB}} + namedo + {{propC}} + f {{/component}} -{{propA}} {{propB}} +{{propA}} +{{propB}} {{propC}}{{propD}} - - {{propE}} {{propF}} - - - {{propG}}{{propH}} - +{{propE}} {{propF}} +{{propG}}{{propH}} hey ================================================================================ @@ -154,12 +129,8 @@ printWidth: 40

Your username is @{{name}}

Hi {{firstName}} {{lastName}}

=====================================output===================================== -

- Your username is @{{name}} -

-

- Hi {{firstName}} {{lastName}} -

+

Your username is @{{name}}

+

Hi {{firstName}} {{lastName}}

================================================================================ `; @@ -183,27 +154,27 @@ Click here! Click here! Click here! Click here! Click here! Click here! =====================================output===================================== + -
- - +
- - + +
================================================================================ @@ -220,16 +191,11 @@ printWidth: 40 =====================================output===================================== {{! TO FIX }} - - - - My tITlE - - My + tITlE - + /> ================================================================================ `; @@ -257,15 +223,17 @@ printWidth: 40 src="/images/pansies.jpg" alt="about fedco bottom image" style="float: left;" - /> - - We are a cooperative - - , one of the few seed companies so organized - in the United States. Because we do not have an individual owner or beneficiary, - profit is not our primary goal. Consumers own 60% of the cooperative and worker - members 40%. Consumer and worker members share proportionately in the cooperative’s - profits through our annual patronage dividends. + />We are a cooperative, + one of the few seed companies so + organized in the United States. + Because we do not have an individual + owner or beneficiary, profit is not + our primary goal. Consumers own 60% of + the cooperative and worker members + 40%. Consumer and worker members share + proportionately in the cooperative’s + profits through our annual patronage + dividends.

================================================================================ `; @@ -295,35 +263,43 @@ conubia nostra, per inceptos himenaeos. Donec in ornare velit.

=====================================output===================================== {{! TO FIX }} -

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce cursus massa vel augue -vestibulum facilisis in porta turpis. Ut faucibus lectus sit amet urna consectetur dignissim. -Sam vitae neque quis ex dapibus faucibus at sed ligula. Nulla sit amet aliquet nibh. -Vestibulum at congue mi. Suspendisse vitae odio vitae massa hendrerit mattis sed eget dui. -Sed eu scelerisque neque. Donec - - maximus - - rhoncus pellentesque. Aenean purus turpis, vehicula -euismod ante vel, ultricies eleifend dui. Class aptent taciti sociosqu ad litora torquent per -conubia nostra, per inceptos himenaeos. Donec in ornare velit. -

+

Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Fusce + cursus massa vel augue vestibulum + facilisis in porta turpis. Ut faucibus + lectus sit amet urna consectetur + dignissim. Sam vitae neque quis ex + dapibus faucibus at sed ligula. Nulla + sit amet aliquet nibh. Vestibulum at + congue mi. Suspendisse vitae odio + vitae massa hendrerit mattis sed eget + dui. Sed eu scelerisque neque. Donec + maximus + rhoncus pellentesque. Aenean purus + turpis, vehicula euismod ante vel, + ultricies eleifend dui. Class aptent + taciti sociosqu ad litora torquent per + conubia nostra, per inceptos + himenaeos. Donec in ornare velit.

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce cursus massa vel augue -vestibulum facilisis in porta turpis. Ut faucibus lectus sit amet urna consectetur dignissim. -Sam vitae neque quis ex dapibus faucibus at sed ligula. Nulla sit amet aliquet nibh. -Vestibulum at congue mi. Suspendisse vitae odio vitae massa hendrerit mattis sed eget dui. -Sed eu scelerisque neque. Donec - - - maximus - - - rhoncus pellentesque. Aenean purus turpis, vehicula -euismod ante vel, ultricies eleifend dui. Class aptent taciti sociosqu ad litora torquent per -conubia nostra, per inceptos himenaeos. Donec in ornare velit. -

+

Lorem ipsum dolor sit amet, + consectetur adipiscing elit. Fusce + cursus massa vel augue vestibulum + facilisis in porta turpis. Ut faucibus + lectus sit amet urna consectetur + dignissim. Sam vitae neque quis ex + dapibus faucibus at sed ligula. Nulla + sit amet aliquet nibh. Vestibulum at + congue mi. Suspendisse vitae odio + vitae massa hendrerit mattis sed eget + dui. Sed eu scelerisque neque. Donec + maximus + rhoncus pellentesque. Aenean purus + turpis, vehicula euismod ante vel, + ultricies eleifend dui. Class aptent + taciti sociosqu ad litora torquent per + conubia nostra, per inceptos + himenaeos. Donec in ornare velit.

================================================================================ `; @@ -342,17 +318,15 @@ printWidth: 40 =====================================output===================================== - - Nihil aut odit omnis. Quam maxime est molestiae. Maxime dolorem dolores voluptas quaerat ut qui sunt vitae error. - +Nihil aut odit omnis. Quam maxime + est molestiae. Maxime dolorem dolores + voluptas quaerat ut qui sunt vitae + error. - - Nihil aut odit omnis. Quam maxime est molestiae. Maxime dolorem dolores voluptas quaerat ut qui sunt vitae error. - +Nihil aut odit omnis. Quam maxime est molestiae. Maxime dolorem dolores voluptas quaerat ut qui sunt vitae error. - - Prix : 32 € - +Prix : 32 € ================================================================================ `; @@ -411,39 +385,45 @@ sometimes{{nogaps}}areimportant {{name}} -Some sentence with {{dynamic -}} expressions. +Some sentence with +{{dynamic}} +expressions. sometimes{{nogaps +}}areimportant -sometimes{{nogaps -}}areimportant - - -{{name}} is your name +{{name}} +is your name {{#block}}{{/block}} {{#block}} - some {{text}} + some + {{text}} {{/block}} {{#block}} - some {{text}} + + some + {{text}} {{/block}} - some {{text}} + some + {{text}} - some {{text}} + + some + {{text}}
+
================================================================================ `; @@ -459,15 +439,14 @@ printWidth: 40 This is your name: {{name}} ({{role}}) =====================================output===================================== - - This is your name: {{name}}. - - - This is your name: {{name}} (employee) - - - This is your name: {{name}} ({{role}}) - +This is your name: + {{name}}. +This is your name: + {{name}} + (employee) +This is your name: + {{name}} + ({{role}}) ================================================================================ `; @@ -496,27 +475,19 @@ printWidth: 40 =====================================output===================================== +123 - 123 - - - 123 - - - 123 + 123 +123 123 +
123
- 123 -
-
- 123 -
-
- 123 + 123
+
123
123 @@ -555,49 +526,23 @@ printWidth: 40 - - - + + +
- A - - B - - C - ABC
- - - - - - - - -
- A - - B - - C -
+
ABC
- - - - - + + +
- A - - B - - C - A B C
diff --git a/tests/html/handlebars-venerable/__snapshots__/jsfmt.spec.js.snap b/tests/html/handlebars-venerable/__snapshots__/jsfmt.spec.js.snap index 6d9eadf0a..c34acbbb7 100644 --- a/tests/html/handlebars-venerable/__snapshots__/jsfmt.spec.js.snap +++ b/tests/html/handlebars-venerable/__snapshots__/jsfmt.spec.js.snap @@ -20,13 +20,8 @@ singleQuote: true =====================================output===================================== | ^^^^^^^^^ - 9 | + 9 | 10 | - 13 | + 13 | =====================================output=====================================