├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── SUGGESTION.md └── workflows │ ├── nodejs-test.yml │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── formatter.js ├── layout.js ├── pkg.js ├── plugins │ ├── indents.js │ ├── multiline.js │ ├── semicolons.js │ └── spaces.js ├── util │ ├── source-code.js │ ├── token-list.js │ └── wrapper.js └── visitors.js └── tests ├── fixtures ├── formatter │ ├── array-literals.txt │ ├── classes.txt │ ├── comments.txt │ ├── config.txt │ ├── destructuring.txt │ ├── empty-statement.txt │ ├── exports.txt │ ├── expressions.txt │ ├── functions.txt │ ├── hashbang.txt │ ├── imports.txt │ ├── keywords.txt │ ├── multiline-function-call.txt │ ├── object-literals.txt │ ├── operators.txt │ ├── statements.txt │ ├── strings.txt │ ├── switch-statement.txt │ ├── template-strings.txt │ ├── trailing-whitespace.txt │ └── variable-declarations.txt ├── raw │ └── config.txt └── token-list │ ├── conditional-multiline.txt │ ├── empty-line-whitespace.txt │ └── template-string-leading-whitespace.txt ├── formatter.test.js ├── layout.test.js └── util └── token-list.test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports ={ 2 | "env":{ 3 | "es6": true, 4 | }, 5 | "extends": "eslint:recommended", 6 | "parserOptions":{ 7 | "ecmaVersion": 2018, 8 | "sourceType": "module" 9 | }, 10 | "rules":{ 11 | "indent":[ 12 | "error", 13 | 4, 14 | { SwitchCase: 1 } 15 | ], 16 | "linebreak-style":[ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes":[ 21 | "error", 22 | "double" 23 | ], 24 | "semi":[ 25 | "error", 26 | "always" 27 | ] 28 | }, 29 | overrides:[ 30 | { 31 | // tests are in commonjs 32 | files:["tests/**/*.js"], 33 | env:{ 34 | mocha:true, 35 | } 36 | } 37 | ] 38 | }; 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41E Report a problem" 3 | about: Report a formatting mistake or fatal error 4 | title: '' 5 | labels: bug, needs repro 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Tell us about your environment** 11 | 12 | * **Nitpik JavaScript Version:** 13 | * **Node Version:** 14 | * **npm Version:** 15 | 16 | **Please show your full configuration:** 17 | 18 |
19 | Configuration 20 | 21 | 22 | ```js 23 | 24 | ``` 25 | 26 |
27 | 28 | **What did you do? Please include the actual source code causing the issue.** 29 | 30 | 31 | ```js 32 | 33 | ``` 34 | 35 | **What did you expect to happen?** 36 | 37 | 38 | **What actually happened? Please include the actual formatted code or console error.** 39 | 40 | 41 | **Are you willing to submit a pull request to fix this bug?** 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUGGESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4DD Suggestion a change" 3 | about: Suggest a change to style options, plugins, or anything else 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **The version of Nitpik JavaSctipt formatter you are using.** 11 | 12 | 13 | **What suggestion do you have for the project?** 14 | 15 | 16 | **Are you willing to submit a pull request to implement this change?** 17 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-test.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | os: [windows-latest, macOS-latest, ubuntu-latest] 13 | node: [12.x] 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - name: npm install, build, and test 22 | run: | 23 | npm install 24 | npm run build --if-present 25 | npm test 26 | env: 27 | CI: true 28 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Package 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish --access public 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | 32 | publish-gpr: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v1 37 | - uses: actions/setup-node@v1 38 | with: 39 | node-version: 12 40 | registry-url: https://npm.pkg.github.com/ 41 | scope: '@nitpik' 42 | - run: npm ci 43 | - run: npm publish --access public 44 | env: 45 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Distributed files 64 | dist/ 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nitpik JavaScript Formatter 2 | 3 | by [Nicholas C. Zakas](https://humanwhocodes.com) 4 | 5 | ![Node CI](https://github.com/nitpik/javascript/workflows/Node%20CI/badge.svg) 6 | 7 | If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate). 8 | 9 | ## Description 10 | 11 | A pluggable JavaScript source code formatter. 12 | 13 | ### Status 14 | 15 | **Prototype** - Seeking feedback and not ready for production use. 16 | 17 | ### Automatic Formatting 18 | 19 | By default, Nitpik JavaScript automatically makes the following changes: 20 | 21 | 1. **Collapses whitespace.** Use a single space anywhere there's more than one space or other whitespace characters. 22 | 2. **Removes trailing whitespace.** Remove whitespace that appears before a line break. 23 | 3. **Normalizes comma spacing.** Spaces before commas are removed and spaces after commas are added where expected (spaces are not added when the comma is immediately followed by a line break). 24 | 4. **Normalizes semicolon spacing.** Spaces before semicolons are removed and spaces after semicolons are added where expected (spaces are not added when the semicolon is immediately followed by a line break). 25 | 26 | ## Usage 27 | 28 | ### Node.js 29 | 30 | Install using [npm][npm] or [yarn][yarn]: 31 | 32 | ``` 33 | npm install @nitpik/javascript --save 34 | 35 | # or 36 | 37 | yarn add @nitpik/javascript 38 | ``` 39 | 40 | Import into your Node.js project: 41 | 42 | ```js 43 | // CommonJS 44 | const { JavaScriptFormatter } = require("@nitpik/javascript"); 45 | 46 | // ESM 47 | import { JavaScriptFormatter } from "@nitpik/javascript"; 48 | ``` 49 | 50 | ### Deno 51 | 52 | Import into your Deno project: 53 | 54 | ```js 55 | import { JavaScriptFormatter } from "https://unpkg.com/@nitpik/javascript/dist/pkg.js"; 56 | ``` 57 | 58 | ### Browser 59 | 60 | Import into a browser script: 61 | 62 | ```js 63 | import { JavaScriptFormatter } from "https://unpkg.com/@nitpik/javascript/dist/pkg.js"; 64 | ``` 65 | 66 | ## API 67 | 68 | After importing, create a new instance of `JavaScriptFormatter`. The constructor accepts one argument which is a configuration object with the following keys: 69 | 70 | * **style** - formatting options 71 | * **collapseWhitespace** - whether multiple spaces in a row should be collapsed into one (default: `true`) 72 | * **emptyLastLine** - should the input end with a line break (default: `true`) 73 | * **indent** - either the character to use for indents or the number of spaces (default: `4`) 74 | * **lineEndings** - the line ending format, either "windows" or "unix" (defualt: `"unix"`) 75 | * **maxEmptyLines** - the maximumn number of empty lines allowed before collapsing (default: `1`) 76 | * **maxLineLength** - the maximum length of a line before wrapping (defualt: `Infinity`) 77 | * **quotes** - the style of quotes to use, either "single" or "double" (default: `"double"`) 78 | * **semicolons** - whether or not to use semicolons (default: `true`) 79 | * **tabWidth** - the number of spaces to count for each tab character (defualt: `4`) 80 | * **trailingCommas** - whether trailing commas should be used for multiline object and array literals (default: `false`) 81 | * **trimTrailingWhitespace** - should trailing whitespace be removed (default: `true`) 82 | * **plugins** - Optional. An array of plugins (see below for examples). 83 | 84 | For example: 85 | 86 | ```js 87 | const formatter = new JavaScriptFormatter({ 88 | style: { 89 | indent: "\t", 90 | quotes: "single" 91 | } 92 | }); 93 | 94 | const result = formatter.format(yourJavaScriptCode); 95 | ``` 96 | 97 | ### Plugins 98 | 99 | A plugin is a function that accepts one parameter, `context`, and returns an object specifying the types of nodes to visit in a JavaScript abstract syntax tree (AST). Here's an example that ensures there's an empty line before each function declaration: 100 | 101 | ```js 102 | function emptyLineBeforeFunctions(context) { 103 | 104 | const { layout } = context; 105 | 106 | return { 107 | FunctionDeclaration(node) { 108 | layout.emptyLineBefore(node); 109 | } 110 | }; 111 | } 112 | ``` 113 | 114 | This function uses the `context.layout` property to specify that there should be an empty line before each function declaration node. `FunctionDeclaration` is the type of node to look for, as defined by [ESTree](https://github.com/estree/estree). The node is passed as an argument to each method as the AST is traversed, so in this example, `node` represents a function declaration. You can then include the function in the `plugins` array of the configuration options: 115 | 116 | ```js 117 | const formatter = new JavaScriptFormatter({ 118 | style: { 119 | indent: "\t", 120 | quotes: "single" 121 | }, 122 | plugins: [ 123 | emptyLineBeforeFunctions 124 | ] 125 | }); 126 | 127 | const result = formatter.format(yourJavaScriptCode); 128 | ``` 129 | 130 | When the formatter is run, it will now run any specified plugins *after* a first-pass of formatting based on the `style` options. This makes it easy to define a default style and then modify it to suit your needs. 131 | 132 | All of the `style` options are implemented internally as plugins. Please see the [`src/plugins`](https://github.com/nitpik/javascript/tree/master/src/plugins) directory for examples (documentation to come later). 133 | 134 | ### Developer Setup 135 | 136 | 1. Ensure you have [Node.js](https://nodejs.org) 12+ installed 137 | 2. Fork and clone this repository 138 | 3. Run `npm install` 139 | 4. Run `npm test` to run tests 140 | 141 | ## License and Copyright 142 | 143 | This code is licensed under the Apache 2.0 License (see LICENSE for details). 144 | 145 | Copyright Human Who Codes LLC. All rights reserved. 146 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nitpik/javascript", 3 | "version": "0.4.0", 4 | "description": "A pluggable JavaScript source code formatter", 5 | "main": "dist/pkg.cjs.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "prepublishOnly": "npm run build", 9 | "lint": "eslint src/ tests/", 10 | "test": "npm run lint && mocha -r esm tests/ --recursive" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/nitpik/javascript.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/nitpick/javascript/issues" 18 | }, 19 | "homepage": "https://github.com/nitpick/javascript#readme", 20 | "keywords": [ 21 | "Nitpik", 22 | "nitpikplugin", 23 | "JavaScript", 24 | "Formatter", 25 | "Beautifier", 26 | "Prettier", 27 | "Code Formatter", 28 | "Style Guide" 29 | ], 30 | "author": "Nicholas C. Zakas", 31 | "license": "Apache-2.0", 32 | "dependencies": { 33 | "@nitpik/toolkit": "^0.1.1", 34 | "espree": "^7.0.0", 35 | "estraverse": "^4.3.0" 36 | }, 37 | "devDependencies": { 38 | "chai": "^4.2.0", 39 | "eslint": "^5.8.0", 40 | "esm": "^3.2.25", 41 | "mocha": "^5.2.0", 42 | "nyc": "^14.1.1", 43 | "rollup": "^1.20.0", 44 | "rollup-plugin-babel-minify": "^10.0.0", 45 | "rollup-plugin-commonjs": "^10.1.0", 46 | "rollup-plugin-json": "^4.0.0", 47 | "rollup-plugin-node-resolve": "^5.2.0" 48 | } 49 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // import minify from "rollup-plugin-babel-minify"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | import json from "rollup-plugin-json"; 5 | 6 | export default [ 7 | { 8 | input: "src/pkg.js", 9 | output: [ 10 | { 11 | file: "dist/pkg.cjs.js", 12 | format: "cjs" 13 | } 14 | ] 15 | }, 16 | { 17 | input: "src/pkg.js", 18 | output: [ 19 | { 20 | file: "dist/pkg.js", 21 | format: "esm", 22 | } 23 | ], 24 | plugins: [resolve(), commonjs(), json()] 25 | }, 26 | 27 | // Commenting out due to babel-minify bug 28 | // { 29 | // input: "src/pkg.js", 30 | // plugins: [minify({ 31 | // comments: false 32 | // })], 33 | // output: { 34 | // file: "dist/pkg.min.js", 35 | // format: "esm" 36 | // } 37 | // } 38 | ]; 39 | -------------------------------------------------------------------------------- /src/formatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Text formatter for JavaScript files. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { Layout } from "./layout.js"; 11 | import espree from "espree"; 12 | import { TaskVisitor } from "./visitors.js"; 13 | import { SourceCode } from "./util/source-code.js"; 14 | 15 | //----------------------------------------------------------------------------- 16 | // Data 17 | //----------------------------------------------------------------------------- 18 | 19 | 20 | //----------------------------------------------------------------------------- 21 | // Helpers 22 | //----------------------------------------------------------------------------- 23 | 24 | class PluginContext { 25 | constructor(text) { 26 | this.text = text; 27 | } 28 | } 29 | 30 | class LayoutPluginContext extends PluginContext { 31 | constructor({ sourceCode, layout }) { 32 | super(sourceCode.text); 33 | this.sourceCode = sourceCode; 34 | this.layout = layout; 35 | } 36 | } 37 | 38 | 39 | //----------------------------------------------------------------------------- 40 | // Exports 41 | //----------------------------------------------------------------------------- 42 | 43 | export class Formatter { 44 | constructor(config = {}) { 45 | this.config = config; 46 | } 47 | 48 | /** 49 | * 50 | * @param {string} text The text to format. 51 | * @param {string} [filePath] The file path the text was read from. 52 | * @returns {string} The formatted source code. 53 | */ 54 | format(text, filePath = "") { 55 | 56 | let hashbang = text.startsWith("#!"); 57 | let textToParse = text; 58 | 59 | // replace hashbang if necessary 60 | if (hashbang) { 61 | textToParse = "//" + text.slice(2); 62 | } 63 | 64 | // TODO: Read parser from config? 65 | const parser = espree; 66 | let ast = parser.parse(textToParse, { 67 | comment: true, 68 | tokens: true, 69 | range: true, 70 | loc: true, 71 | ecmaVersion: espree.latestEcmaVersion || 2019, 72 | sourceType: "module", 73 | ecmaFeatures: { 74 | jsx: true, 75 | globalReturn: true 76 | } 77 | }); 78 | 79 | if (hashbang) { 80 | ast.comments[0].type = "Hashbang"; 81 | ast.comments[0].value = "#!" + ast.comments[0].value.slice(2); 82 | } 83 | 84 | const sourceCode = new SourceCode(text, filePath, ast); 85 | const layout = new Layout(sourceCode, this.config.style); 86 | 87 | if (this.config.plugins) { 88 | const visitor = new TaskVisitor(parser.VisitorKeys); 89 | 90 | for (const plugin of this.config.plugins) { 91 | visitor.addTask(plugin); 92 | } 93 | 94 | visitor.visit(ast, new LayoutPluginContext({ sourceCode, layout })); 95 | } 96 | 97 | return layout.toString(); 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/layout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Utility for laying out JavaScript files. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { TokenList, NEWLINE } from "./util/token-list.js"; 11 | import { Visitor, TaskVisitor } from "./visitors.js"; 12 | import semicolonsTask from "./plugins/semicolons.js"; 13 | import spacesTask from "./plugins/spaces.js"; 14 | import indentsTask from "./plugins/indents.js"; 15 | import multilineTask from "./plugins/multiline.js"; 16 | import espree from "espree"; 17 | 18 | //----------------------------------------------------------------------------- 19 | // Data 20 | //----------------------------------------------------------------------------- 21 | 22 | const LINE_ENDINGS = new Map([ 23 | ["windows", "\r\n"], 24 | ["unix", "\n"] 25 | ]); 26 | 27 | const QUOTES = new Map([ 28 | ["double", "\""], 29 | ["single", "'"], 30 | ]); 31 | 32 | 33 | const DEFAULT_OPTIONS = { 34 | indent: 4, 35 | tabWidth: 4, 36 | lineEndings: "unix", 37 | semicolons: true, 38 | quotes: "double", 39 | collapseWhitespace: true, 40 | trailingCommas: false, 41 | maxEmptyLines: 1, 42 | maxLineLength: Infinity, 43 | trimTrailingWhitespace: true, 44 | emptyLastLine: true 45 | }; 46 | 47 | //----------------------------------------------------------------------------- 48 | // Helpers 49 | //----------------------------------------------------------------------------- 50 | 51 | 52 | 53 | 54 | 55 | /** 56 | * Normalizes the options into a format that `TokenList` can understand. 57 | * @param {Object} options The options to normalize. 58 | * @returns {Object} The modified options object. 59 | */ 60 | function normalizeOptions(options) { 61 | options.indent = (typeof options.indent === "number") ? " ".repeat(options.indent) : options.indent, 62 | options.lineEndings = LINE_ENDINGS.get(options.lineEndings); 63 | options.quotes = QUOTES.get(options.quotes); 64 | return Object.freeze(options); 65 | } 66 | 67 | 68 | function indentBlockComment(part, parts, options) { 69 | 70 | const previousIndent = parts.findPreviousIndent(part); 71 | if (previousIndent && NEWLINE.test(part.value)) { 72 | 73 | // first normalize the new lines and replace with the user preference 74 | let newValue = part.value 75 | .replace(/\r\n/g, "\n") 76 | .replace(NEWLINE, options.lineEndings); 77 | 78 | const originalIndent = parts.getOriginalCommentIndent(part); 79 | part.value = newValue.split(options.lineEndings).map((line, index) => { 80 | 81 | /* 82 | * The first line should never be adjusted because the indent 83 | * is already in the file right before the comment. Similarly, 84 | * other lines that don't already contain the original indent 85 | * should be left alone because they have weird spacing. 86 | */ 87 | return index === 0 || !line.startsWith(originalIndent) 88 | ? line 89 | : previousIndent.value + line.slice(originalIndent.length); 90 | }).join(options.lineEndings); 91 | } 92 | 93 | } 94 | 95 | function normalizeIndentsAndLineBreaks(tokenList, options) { 96 | const indent = options.indent; 97 | const maxEmptyLines = options.maxEmptyLines; 98 | let indentLevel = 0; 99 | let lineBreakCount = 0; 100 | let token = tokenList.first(); 101 | 102 | while (token) { 103 | if (tokenList.isIndentIncreaser(token)) { 104 | indentLevel++; 105 | lineBreakCount = 0; 106 | } else if (tokenList.isIndentDecreaser(token)) { 107 | 108 | /* 109 | * The tricky part about decreasing indent is that the token 110 | * triggering the indent decrease will already be indented at the 111 | * previous level. To fix this, we need to find the first syntax 112 | * on the same line and then adjust the indent before that. 113 | */ 114 | const firstTokenOnLine = tokenList.findFirstTokenOrCommentOnLine(token); 115 | const maybeIndentPart = tokenList.previous(firstTokenOnLine); 116 | 117 | if (tokenList.isIndent(maybeIndentPart)) { 118 | indentLevel--; 119 | 120 | if (indentLevel > 0) { 121 | maybeIndentPart.value = indent.repeat(indentLevel); 122 | } else { 123 | tokenList.delete(maybeIndentPart); 124 | } 125 | } 126 | 127 | lineBreakCount = 0; 128 | 129 | } else if (tokenList.isIndent(token)) { 130 | if (indentLevel > 0) { 131 | token.value = indent.repeat(indentLevel); 132 | } else { 133 | const previousToken = tokenList.previous(token); 134 | tokenList.delete(token); 135 | token = previousToken; 136 | } 137 | } else if (tokenList.isLineBreak(token)) { 138 | 139 | lineBreakCount++; 140 | 141 | if (indentLevel > 0) { 142 | 143 | /* 144 | * If we made it here, it means that there's an indent missing. 145 | * Any line break should be immediately followed by whitespace 146 | * whenever the `indentLevel` is greater than zero. So, here 147 | * we add in the missing whitespace and set it to the appropriate 148 | * indent. 149 | * 150 | * Note that if the next part is a line break, that means the line 151 | * is empty and no extra whitespace should be added. 152 | */ 153 | const peekPart = tokenList.next(token); 154 | if (!tokenList.isWhitespace(peekPart) && !tokenList.isLineBreak(peekPart)) { 155 | tokenList.insertBefore({ 156 | type: "Whitespace", 157 | value: indent.repeat(indentLevel) 158 | }, peekPart); 159 | } 160 | } 161 | 162 | if (lineBreakCount > maxEmptyLines + 1) { 163 | let previousToken = tokenList.previous(token); 164 | tokenList.delete(token); 165 | 166 | if (tokenList.isWhitespace(previousToken)) { 167 | const whitespaceToken = previousToken; 168 | previousToken = tokenList.previous(whitespaceToken); 169 | tokenList.delete(whitespaceToken); 170 | } 171 | 172 | token = previousToken; 173 | lineBreakCount--; 174 | } 175 | 176 | } else if (tokenList.isBlockComment(token)) { 177 | lineBreakCount = 0; 178 | indentBlockComment(token, tokenList, options); 179 | } else if (!tokenList.isWhitespace(token)) { 180 | lineBreakCount = 0; 181 | } 182 | 183 | token = tokenList.next(token); 184 | } 185 | 186 | } 187 | 188 | function ensureEmptyLastLine(tokenList, options) { 189 | let lastToken = tokenList.last(); 190 | if (tokenList.isIndent(lastToken)) { 191 | tokenList.delete(lastToken); 192 | } else if (!tokenList.isLineBreak(lastToken)) { 193 | tokenList.add({ 194 | type: "LineBreak", 195 | value: options.lineEndings 196 | }); 197 | } 198 | } 199 | 200 | function trimTrailingWhitespace(tokenList) { 201 | let token = tokenList.first(); 202 | 203 | while (token) { 204 | 205 | if (tokenList.isLineBreak(token)) { 206 | 207 | const previous = tokenList.previous(token); 208 | if (tokenList.isWhitespace(previous)) { 209 | tokenList.delete(previous); 210 | } 211 | 212 | } 213 | 214 | token = tokenList.next(token); 215 | } 216 | } 217 | 218 | //----------------------------------------------------------------------------- 219 | // Exports 220 | //----------------------------------------------------------------------------- 221 | 222 | export class Layout { 223 | constructor(sourceCode, options = {}) { 224 | this.options = normalizeOptions({ 225 | ...DEFAULT_OPTIONS, 226 | ...options 227 | }); 228 | 229 | let tokenList = TokenList.fromAST(sourceCode.ast, sourceCode.text, this.options); 230 | normalizeIndentsAndLineBreaks(tokenList, this.options); 231 | this.tokenList = tokenList; 232 | let nodeParts = new Map(); 233 | this.nodeParts = nodeParts; 234 | let nodeParents = this.nodeParents = new Map(); 235 | 236 | const visitor = new Visitor(espree.VisitorKeys); 237 | visitor.visit(sourceCode.ast, (node, parent) => { 238 | 239 | nodeParents.set(node, parent); 240 | 241 | const firstToken = tokenList.getByRangeStart(node.range[0]); 242 | 243 | /* 244 | * Program nodes and the body property of Program nodes won't 245 | * have a last part because the end of the range occurs *after* 246 | * the last token. We can just substitue the last code part in 247 | * that case. 248 | */ 249 | let lastToken = tokenList.getByRangeStart(node.range[1]) 250 | ? tokenList.previous(tokenList.getByRangeStart(node.range[1])) 251 | : tokenList.last(); 252 | 253 | /* 254 | * Esprima-style parsers consider the trailing semicolon as the 255 | * last part of a given node. To make life easier when editing, 256 | * we assume the token *before* the semicolon is the last part 257 | * of the node. By doing so, developers can always assume a 258 | * semicolon appears as the next part after the node if present. 259 | */ 260 | if (lastToken.value === ";") { 261 | lastToken = tokenList.previous(lastToken); 262 | 263 | /* 264 | * If a node's last token was previously a semicolon, it's 265 | * possible that it was preceded by whitespace. Whitespace 266 | * between a token and a semicolon insignificant (and often a 267 | * typo), so adjust the last token one more time. 268 | */ 269 | if (tokenList.isWhitespace(lastToken)) { 270 | lastToken = tokenList.previous(lastToken); 271 | } 272 | } 273 | 274 | // automatically remove unneeded empty statements 275 | if (node.type === "EmptyStatement") { 276 | if (Array.isArray(parent.body)) { 277 | parent.body = parent.body.filter(child => child !== node); 278 | tokenList.delete(firstToken); 279 | return; 280 | } 281 | } 282 | 283 | nodeParts.set(node, { 284 | firstToken, 285 | lastToken 286 | }); 287 | }); 288 | 289 | /* 290 | * We need to trim trailing whitespace after all of the rest of the 291 | * processing is done in order to ensure that we've made a correct 292 | * map of nodes to tokens. Removing the whitespace earlier can result 293 | * in tokens missing at the end-range of nodes, which messes up the 294 | * mapping. 295 | */ 296 | if (this.options.trimTrailingWhitespace) { 297 | trimTrailingWhitespace(tokenList); 298 | } 299 | 300 | const tasks = new TaskVisitor(espree.VisitorKeys); 301 | tasks.addTask(semicolonsTask); 302 | tasks.addTask(spacesTask); 303 | tasks.addTask(indentsTask); 304 | tasks.addTask(multilineTask); 305 | tasks.visit(sourceCode.ast, Object.freeze({ sourceCode, layout: this })); 306 | 307 | // now ensure empty last line 308 | if (this.options.emptyLastLine) { 309 | ensureEmptyLastLine(tokenList, this.options); 310 | } 311 | } 312 | 313 | firstToken(tokenOrNode) { 314 | return this.tokenList.has(tokenOrNode) ? tokenOrNode : this.nodeParts.get(tokenOrNode).firstToken; 315 | } 316 | 317 | lastToken(tokenOrNode) { 318 | return this.tokenList.has(tokenOrNode) ? tokenOrNode : this.nodeParts.get(tokenOrNode).lastToken; 319 | } 320 | 321 | boundaryTokens(tokenOrNode) { 322 | return this.tokenList.has(tokenOrNode) 323 | ? { firstToken: tokenOrNode, lastToken: tokenOrNode } 324 | : this.nodeParts.get(tokenOrNode); 325 | } 326 | 327 | nextToken(part) { 328 | return this.tokenList.nextToken(part); 329 | } 330 | 331 | previousToken(part) { 332 | return this.tokenList.previousToken(part); 333 | } 334 | 335 | nextTokenOrComment(part) { 336 | return this.tokenList.nextTokenOrComment(part); 337 | } 338 | 339 | previousTokenOrComment(part) { 340 | return this.tokenList.previousTokenOrComment(part); 341 | } 342 | 343 | isFirstOnLine(startToken) { 344 | let token = this.tokenList.previous(startToken); 345 | while (token) { 346 | if (this.tokenList.isLineBreak(token)) { 347 | return true; 348 | } 349 | 350 | if (!this.tokenList.isComment(token) && !this.tokenList.isWhitespace(token)) { 351 | return false; 352 | } 353 | 354 | token = this.tokenList.previous(token); 355 | } 356 | } 357 | 358 | /** 359 | * Gets number of characters amongst two tokens. 360 | * @param {Token} firstToken The token to start counting from. 361 | * @param {Token} lastToken The last token to count. 362 | * @returns {int} The number of characters among the tokens. 363 | */ 364 | getLength(firstToken, lastToken) { 365 | let currentToken = firstToken; 366 | let characterCount = 0; 367 | 368 | // then count the other tokens 369 | while (currentToken && currentToken !== lastToken) { 370 | characterCount += currentToken.value.length; 371 | currentToken = this.tokenList.next(currentToken); 372 | } 373 | 374 | if (currentToken) { 375 | characterCount += currentToken.value.length; 376 | } 377 | 378 | return characterCount; 379 | } 380 | 381 | /** 382 | * Gets number of characters in the line represented by the token or node. 383 | * @param {Token|Node} tokenOrNode The token or node whose line should be checked. 384 | * @returns {int} The number of characters in the line. 385 | */ 386 | getLineLength(tokenOrNode) { 387 | const token = this.firstToken(tokenOrNode); 388 | let currentToken = this.tokenList.findFirstTokenOrCommentOnLine(token); 389 | const previousToken = this.tokenList.previous(currentToken); 390 | let characterCount = 0; 391 | 392 | // first count the indent, if any 393 | if (this.tokenList.isIndent(previousToken)) { 394 | if (previousToken.value.includes("\t")) { 395 | characterCount += previousToken.value.length * this.options.tabWidth; 396 | } else { 397 | characterCount += previousToken.value.length; 398 | } 399 | } 400 | 401 | // then count the other tokens 402 | while (currentToken && !this.tokenList.isLineBreak(currentToken)) { 403 | characterCount += currentToken.value.length; 404 | currentToken = this.tokenList.next(currentToken); 405 | } 406 | 407 | return characterCount; 408 | } 409 | 410 | isLineTooLong(tokenOrNode) { 411 | const characterCount = this.getLineLength(tokenOrNode); 412 | return characterCount > this.options.maxLineLength; 413 | } 414 | 415 | getIndent(tokenOrNode) { 416 | const firstToken = this.firstToken(tokenOrNode); 417 | let currentToken = this.tokenList.previous(firstToken); 418 | 419 | /* 420 | * If there is no previous token, that means this is the first syntax 421 | * on the first line of the input. Technically, this is a level zero 422 | * indent, so return an object. 423 | */ 424 | if (!currentToken) { 425 | return {}; 426 | } 427 | 428 | /* 429 | * For this loop, we want to see if this node owns an indent. That means 430 | * the start token of the node is the first indented token on the line. 431 | * This is important because it's possible to indent a node that 432 | * doesn't have an indent immediately before it (in which case, the 433 | * parent node is the one that needs indenting). 434 | * 435 | * This loop also skips over comments that are in between the indent 436 | * and the first token. 437 | */ 438 | while (currentToken) { 439 | if (this.tokenList.isIndent(currentToken)) { 440 | return { token: currentToken }; 441 | } 442 | 443 | // first on line but no indent 444 | if (this.tokenList.isLineBreak(currentToken)) { 445 | return {}; 446 | } 447 | 448 | if (!this.tokenList.isComment(currentToken)) { 449 | break; 450 | } 451 | 452 | currentToken = this.tokenList.previous(currentToken); 453 | } 454 | 455 | return undefined; 456 | } 457 | 458 | /** 459 | * Determines the indentation level of the line on which the code starts. 460 | * @param {Token|Node} tokenOrNode The token or node to inspect. 461 | * @returns {int} The zero-based indentation level of the code. 462 | */ 463 | getIndentLevel(tokenOrNode) { 464 | const firstToken = this.firstToken(tokenOrNode); 465 | const lineBreak = this.tokenList.findPreviousLineBreak(firstToken); 466 | const maybeIndent = lineBreak ? this.tokenList.next(lineBreak) : this.tokenList.first(); 467 | 468 | if (this.tokenList.isWhitespace(maybeIndent)) { 469 | return maybeIndent.value.length / this.options.indent.length; 470 | } 471 | 472 | return 0; 473 | } 474 | 475 | /** 476 | * Ensures the given token or node is indented to the specified level. This 477 | * has an effect if the token or node is the first syntax on the line. 478 | * @param {Node} tokenOrNode The token or node to indent. 479 | * @param {int} level The number of levels to indent. 480 | * @returns {boolean} True if the indent was performed, false if not. 481 | */ 482 | indentLevel(tokenOrNode, level) { 483 | 484 | if (typeof level !== "number" || level < 0) { 485 | throw new TypeError("Second argument must be a number >= 0."); 486 | } 487 | 488 | const indent = this.getIndent(tokenOrNode); 489 | 490 | /* 491 | * If the token or node is not the first syntax on a line then we 492 | * should not indent. 493 | */ 494 | if (!indent) { 495 | return false; 496 | } 497 | 498 | let indentToken = indent.token; 499 | const indentText = this.options.indent.repeat(level); 500 | const { firstToken, lastToken } = this.boundaryTokens(tokenOrNode); 501 | 502 | // if there is no indent token, create one 503 | if (!indentToken) { 504 | indentToken = { 505 | type: "Whitespace", 506 | value: "" 507 | }; 508 | 509 | const lineBreak = this.tokenList.findPreviousLineBreak(firstToken); 510 | if (lineBreak) { 511 | this.tokenList.insertAfter(indentToken, lineBreak); 512 | } else { 513 | this.tokenList.insertBefore(indentToken, firstToken); 514 | } 515 | } 516 | 517 | indentToken.value = indentText; 518 | 519 | // find remaining indents in this node and update as well 520 | let token = firstToken; 521 | while (token !== lastToken) { 522 | if (this.tokenList.isIndent(token)) { 523 | // make sure to keep relative indents correct 524 | token.value = indentText + token.value.slice(indentText.length); 525 | } 526 | token = this.tokenList.next(token); 527 | } 528 | 529 | return true; 530 | } 531 | 532 | /** 533 | * Ensures all indents between the two tokens are set to the given level. 534 | * @param {Token} firstToken The first token to indent. 535 | * @param {Token} lastToken The last token to indent. 536 | * @param {int} level The number of levels to indent. 537 | * @returns {boolean} True if the indent was performed, false if not. 538 | */ 539 | indentLevelBetween(firstToken, lastToken, level) { 540 | 541 | if (typeof level !== "number" || level < 0) { 542 | throw new TypeError("Third argument must be a number >= 0."); 543 | } 544 | 545 | const indent = this.getIndent(firstToken); 546 | 547 | /* 548 | * If the token or node is not the first syntax on a line then we 549 | * should not indent. 550 | */ 551 | if (!indent) { 552 | return false; 553 | } 554 | 555 | let indentToken = indent.token; 556 | const indentText = this.options.indent.repeat(level); 557 | 558 | // if there is no indent token, create one 559 | if (!indentToken) { 560 | indentToken = { 561 | type: "Whitespace", 562 | value: "" 563 | }; 564 | 565 | const lineBreak = this.tokenList.findPreviousLineBreak(firstToken); 566 | if (lineBreak) { 567 | this.tokenList.insertAfter(indentToken, lineBreak); 568 | } else { 569 | this.tokenList.insertBefore(indentToken, firstToken); 570 | } 571 | } 572 | 573 | indentToken.value = indentText; 574 | 575 | // find remaining indents in this node and update as well 576 | let token = firstToken; 577 | while (token && token !== lastToken) { 578 | 579 | if (this.tokenList.isIndent(token)) { 580 | // make sure to keep relative indents correct 581 | token.value = indentText + token.value.slice(indentText.length); 582 | } else if (this.tokenList.isLineBreak(token)) { 583 | 584 | /* 585 | * It's possible a node that should be indented doesn't already 586 | * have an indent. The only way to know is to check to see if the 587 | * next node after a line break is whitespace. If not, then create 588 | * one. The created indent will be adjusted in the first part of this 589 | * while loop. 590 | */ 591 | const maybeIndent = this.tokenList.next(token); 592 | if (!this.tokenList.isIndent(maybeIndent) && !this.tokenList.isLineBreak(maybeIndent)) { 593 | this.tokenList.insertAfter({ 594 | type: "Whitespace", 595 | value: "" 596 | }, token); 597 | } 598 | 599 | } 600 | token = this.tokenList.next(token); 601 | } 602 | 603 | return true; 604 | } 605 | 606 | /** 607 | * Indents the given node only if the node is the first syntax on the line. 608 | * @param {Node} tokenOrNode The token or node to indent. 609 | * @param {int} [levels=1] The number of levels to indent. If this value is 610 | * 0 then it is considered to be 1. Negative numbers decrease indentation. 611 | * returns {boolean} True if the indent was performed, false if not. 612 | */ 613 | indent(tokenOrNode, levels = 1) { 614 | const indentPart = this.getIndent(tokenOrNode); 615 | if (!indentPart) { 616 | return false; 617 | 618 | } 619 | 620 | // normalize levels 621 | if (levels === 0) { 622 | levels = 1; 623 | } 624 | 625 | const effectiveIndent = this.options.indent.repeat(Math.abs(levels)); 626 | let indentToken = indentPart.token; 627 | const { firstToken, lastToken } = this.boundaryTokens(tokenOrNode); 628 | 629 | // if there is no indent token, create one 630 | if (!indentToken) { 631 | indentToken = { 632 | type: "Whitespace", 633 | value: "" 634 | }; 635 | 636 | const lineBreak = this.tokenList.findPreviousLineBreak(firstToken); 637 | if (lineBreak) { 638 | this.tokenList.insertAfter(indentToken, lineBreak); 639 | } else { 640 | this.tokenList.insertBefore(indentToken, firstToken); 641 | } 642 | } 643 | 644 | // calculate new indent and update indent token 645 | const newIndent = levels > 0 646 | ? indentToken.value + effectiveIndent 647 | : indentToken.value.slice(effectiveIndent.length); 648 | indentToken.value = newIndent; 649 | 650 | // find remaining indents in this node and update as well 651 | let token = firstToken; 652 | while (token !== lastToken) { 653 | if (this.tokenList.isIndent(token)) { 654 | token.value = newIndent; 655 | } 656 | token = this.tokenList.next(token); 657 | } 658 | 659 | return true; 660 | } 661 | 662 | 663 | /** 664 | * Determines if a given node's syntax spans multiple lines. 665 | * @param {Node} node The node to check. 666 | * @returns {boolean} True if the node spans multiple lines, false if not. 667 | */ 668 | isMultiLine(node) { 669 | const { firstToken, lastToken } = this.boundaryTokens(node); 670 | let token = this.tokenList.next(firstToken); 671 | 672 | while (token !== lastToken) { 673 | if (this.tokenList.isLineBreak(token)) { 674 | return true; 675 | } 676 | 677 | token = this.tokenList.next(token); 678 | } 679 | 680 | return false; 681 | } 682 | 683 | isSameLine(firstPartOrNode, secondPartOrNode) { 684 | const startToken = this.lastToken(firstPartOrNode); 685 | const endToken = this.firstToken(secondPartOrNode); 686 | let token = this.tokenList.next(startToken); 687 | 688 | while (token && token !== endToken) { 689 | if (this.tokenList.isLineBreak(token)) { 690 | return false; 691 | } 692 | 693 | token = this.tokenList.next(token); 694 | } 695 | 696 | return Boolean(token); 697 | } 698 | 699 | findNext(valueOrFunction, partOrNode) { 700 | const matcher = typeof valueOrFunction === "string" 701 | ? part => part.value === valueOrFunction 702 | : valueOrFunction; 703 | const part = partOrNode ? this.lastToken(partOrNode) : this.tokenList.first(); 704 | return this.tokenList.findNext(matcher, part); 705 | } 706 | 707 | findPrevious(valueOrFunction, partOrNode) { 708 | const matcher = typeof valueOrFunction === "string" 709 | ? part => part.value === valueOrFunction 710 | : valueOrFunction; 711 | const part = partOrNode ? this.firstToken(partOrNode) : this.tokenList.last(); 712 | return this.tokenList.findPrevious(matcher, part); 713 | } 714 | 715 | spaceBefore(tokenOrNode) { 716 | 717 | let firstToken = this.firstToken(tokenOrNode); 718 | 719 | const previousToken = this.tokenList.previous(firstToken); 720 | if (previousToken) { 721 | if(!this.tokenList.isIndent(previousToken)) { 722 | if (this.tokenList.isWhitespace(previousToken)) { 723 | previousToken.value = " "; 724 | return true; 725 | } else if (!this.tokenList.isLineBreak(previousToken)) { 726 | this.tokenList.insertBefore({ 727 | type: "Whitespace", 728 | value: " " 729 | }, firstToken); 730 | return true; 731 | } 732 | } 733 | } else { 734 | this.tokenList.insertBefore({ 735 | type: "Whitespace", 736 | value: " " 737 | }, firstToken); 738 | return true; 739 | } 740 | 741 | return false; 742 | } 743 | 744 | spaceAfter(partOrNode) { 745 | let lastToken = this.lastToken(partOrNode); 746 | 747 | const nextToken = this.tokenList.next(lastToken); 748 | if (nextToken) { 749 | if (this.tokenList.isWhitespace(nextToken)) { 750 | nextToken.value = " "; 751 | return true; 752 | } else if (!this.tokenList.isLineBreak(nextToken)) { 753 | this.tokenList.insertAfter({ 754 | type: "Whitespace", 755 | value: " " 756 | }, lastToken); 757 | return true; 758 | } 759 | } 760 | 761 | return false; 762 | } 763 | 764 | spaces(partOrNode) { 765 | const afterResult = this.spaceAfter(partOrNode); 766 | const beforeResult = this.spaceBefore(partOrNode); 767 | return afterResult || beforeResult; 768 | } 769 | 770 | noSpaceAfter(partOrNode) { 771 | let part = this.lastToken(partOrNode); 772 | 773 | const next = this.tokenList.next(part); 774 | if (next && this.tokenList.isWhitespace(next)) { 775 | this.tokenList.delete(next); 776 | return true; 777 | } 778 | 779 | return false; 780 | } 781 | 782 | noSpaceBefore(partOrNode) { 783 | let part = this.firstToken(partOrNode); 784 | 785 | const previous = this.tokenList.previous(part); 786 | if (previous && this.tokenList.isWhitespace(previous) && !this.tokenList.isIndent(previous)) { 787 | this.tokenList.delete(previous); 788 | return true; 789 | } 790 | 791 | return false; 792 | } 793 | 794 | noSpaces(partOrNode) { 795 | const afterResult = this.noSpaceAfter(partOrNode); 796 | const beforeResult = this.noSpaceBefore(partOrNode); 797 | return afterResult || beforeResult; 798 | } 799 | 800 | semicolonAfter(partOrNode) { 801 | let part = this.lastToken(partOrNode); 802 | 803 | // check to see what the next code part is 804 | const next = this.tokenList.next(part); 805 | if (next) { 806 | if (next.type !== "Punctuator" || next.value !== ";") { 807 | this.tokenList.insertAfter({ 808 | type: "Punctuator", 809 | value: ";", 810 | }, part); 811 | return true; 812 | } 813 | } else if (!this.tokenList.isLineBreak(part)) { 814 | 815 | /* 816 | * We are at the end of the file, so just add the semicolon 817 | * but only if the last part of the file isn't a line break. 818 | */ 819 | this.tokenList.add({ 820 | type: "Punctuator", 821 | value: ";" 822 | }); 823 | return true; 824 | } 825 | 826 | return false; 827 | } 828 | 829 | noSemicolonAfter(partOrNode) { 830 | let part = this.lastToken(partOrNode); 831 | 832 | // check to see what the next code part is 833 | const next = this.tokenList.next(part); 834 | if (next) { 835 | if (next.value === ";") { 836 | 837 | // can only remove if there's a line break or EOF 838 | const maybeLineBreak = this.tokenList.next(next); 839 | 840 | if (!maybeLineBreak || this.tokenList.isLineBreak(maybeLineBreak)) { 841 | this.tokenList.delete(next); 842 | return true; 843 | } 844 | 845 | } 846 | } 847 | 848 | return false; 849 | } 850 | 851 | /** 852 | * Ensures that there is a comma after a given token or node. 853 | * @param {Token|Node} tokenOrNode The token or node to look for a comma 854 | * after. 855 | * @returns {boolean} True if a comma was added, false if not. 856 | */ 857 | commaAfter(partOrNode) { 858 | let part = this.lastToken(partOrNode); 859 | 860 | // check to see what the next code part is 861 | const next = this.nextToken(part); 862 | if (next) { 863 | 864 | // don't insert after another comma 865 | if (next.value !== ",") { 866 | this.tokenList.insertAfter({ 867 | type: "Punctuator", 868 | value: ",", 869 | }, part); 870 | 871 | return true; 872 | } 873 | } 874 | 875 | /* 876 | * If we make it to here, then we're at the end of the file and a comma 877 | * should not be inserted because it's likely not valid syntax. 878 | */ 879 | return false; 880 | } 881 | 882 | /** 883 | * Ensures that there is no comma after a given token or node. 884 | * @param {Token|Node} tokenOrNode The token or node to look for a comma 885 | * after. 886 | * @returns {boolean} True if a comma was deleted, false if not. 887 | */ 888 | noCommaAfter(tokenOrNode) { 889 | let firstToken = this.lastToken(tokenOrNode); 890 | 891 | // check to see what the next token is 892 | const next = this.nextToken(firstToken); 893 | if (next && next.value === ",") { 894 | this.tokenList.delete(next); 895 | return true; 896 | } 897 | 898 | /* 899 | * If we make it to here, then we're at the end of the file and a comma 900 | * should not be inserted because it's likely not valid syntax. 901 | */ 902 | return false; 903 | } 904 | 905 | emptyLineBefore(tokenOrNode) { 906 | let token = this.firstToken(tokenOrNode); 907 | const previousToken = this.tokenList.previous(token); 908 | 909 | if (previousToken) { 910 | 911 | // if there's already a line break see if there's another 912 | if (this.tokenList.isLineBreak(previousToken)) { 913 | 914 | const earlierToken = this.tokenList.previous(previousToken); 915 | 916 | if (this.tokenList.isLineBreak(earlierToken)) { 917 | return false; 918 | } 919 | 920 | this.tokenList.insertBefore({ 921 | type: "LineBreak", 922 | value: this.options.lineEndings 923 | }, token); 924 | 925 | return true; 926 | 927 | } else if (!this.tokenList.isIndent(previousToken)) { 928 | this.tokenList.insertBefore({ 929 | type: "LineBreak", 930 | value: this.options.lineEndings 931 | }, token); 932 | 933 | this.tokenList.insertBefore({ 934 | type: "LineBreak", 935 | value: this.options.lineEndings 936 | }, token); 937 | 938 | // trim trailing whitespace if necessary 939 | if (this.options.trimTrailingWhitespace && this.tokenList.isWhitespace(previousToken)) { 940 | this.tokenList.delete(previousToken); 941 | } 942 | 943 | return true; 944 | } 945 | 946 | } else { 947 | 948 | this.tokenList.insertBefore({ 949 | type: "LineBreak", 950 | value: this.options.lineEndings 951 | }, token); 952 | 953 | return true; 954 | } 955 | 956 | return false; 957 | } 958 | 959 | emptyLineAfter(tokenOrNode) { 960 | let token = this.lastToken(tokenOrNode); 961 | 962 | let next = this.tokenList.next(token); 963 | if (next) { 964 | 965 | if (this.tokenList.isLineBreak(next)) { 966 | 967 | // There is at least one line break so see if we need more 968 | next = this.tokenList.next(next); 969 | 970 | // skip over any whitespace 971 | if (this.tokenList.isWhitespace(next)) { 972 | next = this.tokenList.next(next); 973 | } 974 | 975 | if (!this.tokenList.isLineBreak(next)) { 976 | this.tokenList.insertAfter({ 977 | type: "LineBreak", 978 | value: this.options.lineEndings 979 | }, token); 980 | 981 | return true; 982 | } 983 | 984 | return false; 985 | 986 | } else { 987 | 988 | // There are no line breaks after the token so insert two 989 | 990 | this.tokenList.insertAfter({ 991 | type: "LineBreak", 992 | value: this.options.lineEndings 993 | }, token); 994 | 995 | this.tokenList.insertAfter({ 996 | type: "LineBreak", 997 | value: this.options.lineEndings 998 | }, token); 999 | } 1000 | 1001 | return true; 1002 | 1003 | } else { 1004 | this.tokenList.insertAfter({ 1005 | type: "LineBreak", 1006 | value: this.options.lineEndings 1007 | }, token); 1008 | 1009 | this.tokenList.insertAfter({ 1010 | type: "LineBreak", 1011 | value: this.options.lineEndings 1012 | }, token); 1013 | 1014 | return true; 1015 | } 1016 | 1017 | } 1018 | 1019 | noEmptyLineAfter(tokenOrNode) { 1020 | let token = this.lastToken(tokenOrNode); 1021 | let maybeLineBreak = this.tokenList.next(token); 1022 | 1023 | if (maybeLineBreak) { 1024 | // skip over semicolons 1025 | if (maybeLineBreak.value === ";") { 1026 | maybeLineBreak = this.tokenList.next(maybeLineBreak); 1027 | } 1028 | 1029 | if (this.tokenList.isLineBreak(maybeLineBreak)) { 1030 | 1031 | let whitespace = null; 1032 | maybeLineBreak = this.tokenList.next(maybeLineBreak); 1033 | if (this.tokenList.isWhitespace(maybeLineBreak)) { 1034 | whitespace = maybeLineBreak; 1035 | maybeLineBreak = this.tokenList.next(maybeLineBreak); 1036 | } 1037 | 1038 | if (this.tokenList.isLineBreak(maybeLineBreak)) { 1039 | // make sure to delete any preceding whitespace too 1040 | if (whitespace) { 1041 | this.tokenList.delete(whitespace); 1042 | } 1043 | 1044 | this.tokenList.delete(maybeLineBreak); 1045 | 1046 | return true; 1047 | } 1048 | } 1049 | } 1050 | 1051 | return false; 1052 | } 1053 | 1054 | noEmptyLineBefore(tokenOrNode) { 1055 | let token = this.firstToken(tokenOrNode); 1056 | let maybeLineBreak = this.tokenList.previous(token); 1057 | 1058 | if (maybeLineBreak) { 1059 | // skip over whitespace 1060 | if (this.tokenList.isWhitespace(maybeLineBreak)) { 1061 | maybeLineBreak = this.tokenList.previous(maybeLineBreak); 1062 | } 1063 | 1064 | if (this.tokenList.isLineBreak(maybeLineBreak)) { 1065 | 1066 | // TODO: Refactor this logic 1067 | 1068 | // check for beginning of file 1069 | if (this.tokenList.first() !== maybeLineBreak) { 1070 | 1071 | // check for preceding whitespace too 1072 | let whitespace = null; 1073 | maybeLineBreak = this.tokenList.previous(maybeLineBreak); 1074 | if (this.tokenList.isWhitespace(maybeLineBreak)) { 1075 | whitespace = maybeLineBreak; 1076 | maybeLineBreak = this.tokenList.previous(maybeLineBreak); 1077 | } 1078 | 1079 | // only if we find a second line break do we need to act 1080 | if (this.tokenList.isLineBreak(maybeLineBreak)) { 1081 | 1082 | // make sure to delete any preceding whitespace too 1083 | if (whitespace) { 1084 | this.tokenList.delete(whitespace); 1085 | } 1086 | 1087 | this.tokenList.delete(maybeLineBreak); 1088 | 1089 | return true; 1090 | } 1091 | } else { 1092 | this.tokenList.delete(maybeLineBreak); 1093 | return true; 1094 | } 1095 | 1096 | } 1097 | 1098 | return false; 1099 | } 1100 | } 1101 | 1102 | lineBreakAfter(tokenOrNode) { 1103 | let token = this.lastToken(tokenOrNode); 1104 | 1105 | const next = this.tokenList.next(token); 1106 | if (next) { 1107 | if (!this.tokenList.isLineBreak(next)) { 1108 | this.tokenList.insertAfter({ 1109 | type: "LineBreak", 1110 | value: this.options.lineEndings 1111 | }, token); 1112 | return true; 1113 | } 1114 | } else { 1115 | this.tokenList.insertAfter({ 1116 | type: "LineBreak", 1117 | value: this.options.lineEndings 1118 | }, token); 1119 | return true; 1120 | } 1121 | 1122 | return false; 1123 | } 1124 | 1125 | noLineBreakAfter(tokenOrNode) { 1126 | let token = this.lastToken(tokenOrNode); 1127 | 1128 | const lineBreak = this.tokenList.next(token); 1129 | if (lineBreak) { 1130 | if (this.tokenList.isLineBreak(lineBreak)) { 1131 | this.tokenList.delete(lineBreak); 1132 | 1133 | // collapse whitespace if necessary 1134 | const nextToken = this.tokenList.next(token); 1135 | if (this.tokenList.isWhitespace(nextToken) && this.options.collapseWhitespace) { 1136 | nextToken.value = " "; 1137 | } 1138 | } 1139 | } 1140 | } 1141 | 1142 | lineBreakBefore(tokenOrNode) { 1143 | let token = this.firstToken(tokenOrNode); 1144 | const previousToken = this.tokenList.previous(token); 1145 | 1146 | if (previousToken) { 1147 | if (!this.tokenList.isLineBreak(previousToken) && !this.tokenList.isIndent(previousToken)) { 1148 | this.tokenList.insertBefore({ 1149 | type: "LineBreak", 1150 | value: this.options.lineEndings 1151 | }, token); 1152 | 1153 | // trim trailing whitespace if necessary 1154 | if (this.options.trimTrailingWhitespace && this.tokenList.isWhitespace(previousToken)) { 1155 | this.tokenList.delete(previousToken); 1156 | } 1157 | 1158 | } 1159 | 1160 | } 1161 | } 1162 | 1163 | noLineBreakBefore(tokenOrNode) { 1164 | const token = this.firstToken(tokenOrNode); 1165 | let previousToken = this.tokenList.previous(token); 1166 | 1167 | if (previousToken) { 1168 | 1169 | // TODO: Maybe figure out if indent should be deleted or converted to one space? 1170 | // delete any indent 1171 | if (this.tokenList.isIndent(previousToken)) { 1172 | this.tokenList.delete(previousToken); 1173 | previousToken = this.tokenList.previous(token); 1174 | } 1175 | 1176 | if (this.tokenList.isLineBreak(previousToken)) { 1177 | this.tokenList.delete(previousToken); 1178 | } 1179 | 1180 | } 1181 | } 1182 | 1183 | toString() { 1184 | return [...this.tokenList].map(part => part.value).join(""); 1185 | } 1186 | } 1187 | -------------------------------------------------------------------------------- /src/pkg.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | //----------------------------------------------------------------------------- 4 | // Exports 5 | //----------------------------------------------------------------------------- 6 | 7 | export { Formatter as JavaScriptFormatter } from "./formatter.js"; 8 | -------------------------------------------------------------------------------- /src/plugins/indents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A task to automatically adjust indents as needed. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Helpers 8 | //----------------------------------------------------------------------------- 9 | 10 | 11 | //----------------------------------------------------------------------------- 12 | // Task 13 | //----------------------------------------------------------------------------- 14 | 15 | export default function(context) { 16 | const layout = context.layout; 17 | 18 | 19 | function indentNonBlockBody(node, body) { 20 | if (body.type === "ExpressionStatement" && layout.isMultiLine(node)) { 21 | const indentLevel = layout.getIndentLevel(node); 22 | layout.indentLevel(body, indentLevel + 1); 23 | } 24 | } 25 | 26 | return { 27 | 28 | ForStatement(node) { 29 | indentNonBlockBody(node, node.body); 30 | }, 31 | 32 | ForInStatement(node) { 33 | indentNonBlockBody(node, node.body); 34 | }, 35 | 36 | ForOfStatement(node) { 37 | indentNonBlockBody(node, node.body); 38 | }, 39 | 40 | IfStatement(node) { 41 | indentNonBlockBody(node, node.consequent); 42 | }, 43 | 44 | SwitchCase(node) { 45 | const indentLevel = layout.getIndentLevel(node); 46 | node.consequent.forEach(child => { 47 | if (child.type !== "BlockStatement") { 48 | layout.indentLevel(child, indentLevel + 1); 49 | } 50 | }); 51 | }, 52 | 53 | WhileStatement(node) { 54 | indentNonBlockBody(node, node.body); 55 | }, 56 | 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/plugins/multiline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A task to figure out multi- vs single-line layout. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { Wrapper } from "../util/wrapper.js"; 11 | 12 | //----------------------------------------------------------------------------- 13 | // Helpers 14 | //----------------------------------------------------------------------------- 15 | 16 | const binaries = new Set([ 17 | "BinaryExpression", 18 | "LogicalExpression" 19 | ]); 20 | 21 | 22 | function isMemberExpression(node) { 23 | return Boolean(node && node.type === "MemberExpression"); 24 | } 25 | 26 | 27 | //----------------------------------------------------------------------------- 28 | // Task 29 | //----------------------------------------------------------------------------- 30 | 31 | export default function(context) { 32 | const layout = context.layout; 33 | const wrapper = new Wrapper(context); 34 | 35 | function wrapIfTooLong(node) { 36 | if (layout.isLineTooLong(node)) { 37 | wrapper.wrap(node); 38 | } 39 | } 40 | 41 | function wrapIfTooLongOrMultiLine(node) { 42 | if (layout.isMultiLine(node) || layout.isLineTooLong(node)) { 43 | wrapper.wrap(node); 44 | } 45 | } 46 | 47 | return { 48 | ArrayExpression(node) { 49 | const isMultiLine = layout.isMultiLine(node); 50 | if (node.elements.length) { 51 | if (layout.isLineTooLong(node) || isMultiLine) { 52 | wrapper.wrap(node); 53 | } else if (!isMultiLine) { 54 | wrapper.unwrap(node); 55 | } 56 | } else { 57 | wrapper.unwrap(node); 58 | } 59 | }, 60 | 61 | ArrayPattern(node) { 62 | this.ArrayExpression(node); 63 | }, 64 | 65 | ArrowFunctionExpression(node, parent) { 66 | this.FunctionExpression(node, parent); 67 | }, 68 | 69 | BinaryExpression(node, parent) { 70 | if (layout.isMultiLine(node) || layout.isLineTooLong(node) || 71 | (binaries.has(parent.type) && layout.isMultiLine(parent)) 72 | ) { 73 | wrapper.wrap(node); 74 | } 75 | }, 76 | 77 | CallExpression(node, parent) { 78 | // covers chained member expressions like `a.b().c()` 79 | if (isMemberExpression(parent) && layout.isMultiLine(parent) && 80 | isMemberExpression(node.callee) 81 | ) { 82 | wrapper.wrap(node.callee); 83 | } 84 | 85 | const firstArgOnDifferentLine = node.arguments.length && !layout.isSameLine(node.callee, node.arguments[0]); 86 | 87 | // covers long calls like `foo(bar, baz)` 88 | if (layout.isLineTooLong(node) || firstArgOnDifferentLine) { 89 | wrapper.wrap(node); 90 | } 91 | // wrapIfTooLong(node); 92 | }, 93 | 94 | ConditionalExpression: wrapIfTooLongOrMultiLine, 95 | 96 | DoWhileStatement(node) { 97 | 98 | /* 99 | * Because the condition is on the last list of a do-while loop 100 | * we need to check if the last line is too long rather than the 101 | * first line. 102 | */ 103 | const openParen = layout.findPrevious("(", node.test); 104 | if (layout.isLineTooLong(openParen)) { 105 | wrapper.wrap(node); 106 | } 107 | }, 108 | 109 | ExportNamedDeclaration: wrapIfTooLongOrMultiLine, 110 | 111 | FunctionDeclaration(node) { 112 | this.FunctionExpression(node); 113 | }, 114 | 115 | FunctionExpression: wrapIfTooLongOrMultiLine, 116 | IfStatement: wrapIfTooLong, 117 | ImportDeclaration: wrapIfTooLongOrMultiLine, 118 | 119 | LogicalExpression(node, parent) { 120 | this.BinaryExpression(node, parent); 121 | }, 122 | 123 | MemberExpression(node, parent) { 124 | 125 | // covers chained member calls like `a.b.c` 126 | if ( 127 | layout.isMultiLine(node) || layout.isLineTooLong(node) || 128 | (isMemberExpression(parent) && layout.isMultiLine(parent)) 129 | ) { 130 | wrapper.wrap(node); 131 | } 132 | }, 133 | 134 | TemplateLiteral: wrapIfTooLong, 135 | 136 | ObjectExpression(node) { 137 | const isMultiLine = layout.isMultiLine(node); 138 | if (node.properties.length) { 139 | if (layout.isLineTooLong(node) || isMultiLine) { 140 | wrapper.wrap(node); 141 | } else if (!isMultiLine) { 142 | wrapper.unwrap(node); 143 | } 144 | } else { 145 | wrapper.unwrap(node); 146 | } 147 | }, 148 | 149 | ObjectPattern(node) { 150 | this.ObjectExpression(node); 151 | }, 152 | 153 | VariableDeclaration: wrapIfTooLongOrMultiLine, 154 | WhileStatement: wrapIfTooLong, 155 | 156 | }; 157 | } 158 | -------------------------------------------------------------------------------- /src/plugins/semicolons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A task to automatically adjust semicolons as needed. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Data 8 | //----------------------------------------------------------------------------- 9 | 10 | const variableDeclarationExceptions = new Set([ 11 | "ForInStatement", 12 | "ForOfStatement", 13 | ]); 14 | 15 | //----------------------------------------------------------------------------- 16 | // Task 17 | //----------------------------------------------------------------------------- 18 | 19 | export default function(context) { 20 | const layout = context.layout; 21 | const semicolons = layout.options.semicolons; 22 | 23 | function adjustSemicolon(node) { 24 | if (semicolons) { 25 | layout.semicolonAfter(node); 26 | } else { 27 | layout.noSemicolonAfter(node); 28 | } 29 | } 30 | 31 | return { 32 | ExpressionStatement: adjustSemicolon, 33 | ReturnStatement: adjustSemicolon, 34 | ThrowStatement: adjustSemicolon, 35 | DoWhileStatement: adjustSemicolon, 36 | DebuggerStatement: adjustSemicolon, 37 | BreakStatement: adjustSemicolon, 38 | ContinueStatement: adjustSemicolon, 39 | ImportDeclaration: adjustSemicolon, 40 | ExportAllDeclaration: adjustSemicolon, 41 | ExportNamedDeclaration(node) { 42 | 43 | // declarations never need a semicolon 44 | if(!node.declaration) { 45 | adjustSemicolon(node); 46 | } 47 | 48 | }, 49 | ExportDefaultDeclaration(node) { 50 | if (!/(?:Class|Function)Declaration/u.test(node.declaration.type)) { 51 | adjustSemicolon(node); 52 | } 53 | }, 54 | VariableDeclaration(node, parent) { 55 | 56 | if (!variableDeclarationExceptions.has(parent.type) || parent.left !== node) { 57 | adjustSemicolon(node); 58 | } 59 | } 60 | 61 | }; 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/plugins/spaces.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview A task to automatically adjust spaces as needed. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Helpers 8 | //----------------------------------------------------------------------------- 9 | 10 | function findNextCommaOrSemicolon(layout, start) { 11 | return layout.findNext(part => part.type === "Punctuator", start); 12 | } 13 | 14 | function normalizePunctuatorSpacing(layout) { 15 | let token = findNextCommaOrSemicolon(layout); 16 | while (token) { 17 | 18 | switch (token.value) { 19 | case ",": 20 | case ";": 21 | layout.noSpaceBefore(token); 22 | layout.spaceAfter(token); 23 | break; 24 | 25 | case ".": 26 | layout.noSpaces(token); 27 | break; 28 | 29 | default: 30 | if (token.value.includes("=")) { 31 | layout.spaceBefore(token); 32 | layout.spaceAfter(token); 33 | } 34 | } 35 | 36 | token = findNextCommaOrSemicolon(layout, token); 37 | } 38 | } 39 | 40 | function spaceKeywordAndBrace(node, bodyKey, layout) { 41 | const firstToken = layout.firstToken(node); 42 | layout.spaceAfter(firstToken); 43 | 44 | const braceToken = layout.firstToken(node[bodyKey]); 45 | if (braceToken.value === "{") { 46 | layout.spaceBefore(braceToken); 47 | } 48 | 49 | } 50 | 51 | //----------------------------------------------------------------------------- 52 | // Task 53 | //----------------------------------------------------------------------------- 54 | 55 | export default function(context) { 56 | const layout = context.layout; 57 | 58 | // first, adjust all commas 59 | normalizePunctuatorSpacing(layout); 60 | 61 | 62 | return { 63 | 64 | ArrayExpression(node) { 65 | 66 | const { firstToken, lastToken } = layout.boundaryTokens(node); 67 | layout.noSpaceAfter(firstToken); 68 | 69 | // no spacing work for multiline 70 | if (!layout.isMultiLine(node)) { 71 | 72 | layout.noSpaceBefore(lastToken); 73 | 74 | if (node.elements.length) { 75 | 76 | node.elements.forEach((element, index) => { 77 | 78 | if (index > 0) { 79 | layout.spaceBefore(element); 80 | } 81 | layout.noSpaceAfter(element); 82 | }); 83 | } 84 | } 85 | }, 86 | 87 | ArrayPattern(node) { 88 | this.ArrayExpression(node); 89 | }, 90 | 91 | ArrowFunctionExpression(node) { 92 | 93 | let openParenToken, closeParenToken; 94 | const firstToken = layout.firstToken(node); 95 | 96 | if (node.async) { 97 | layout.spaceAfter(firstToken); 98 | } 99 | 100 | if (node.params.length === 0) { 101 | 102 | openParenToken = node.async 103 | ? layout.findNext("(", firstToken) 104 | : firstToken; 105 | 106 | closeParenToken = layout.findNext(")", openParenToken); 107 | } else if (node.params.length === 1) { 108 | 109 | if (node.async) { 110 | layout.spaceAfter(firstToken); 111 | openParenToken = layout.findPrevious(part => { 112 | return part === firstToken || part.value === "("; 113 | }, node.params[0]); 114 | 115 | if (openParenToken.value !== "(") { 116 | openParenToken = null; 117 | } else { 118 | closeParenToken = layout.findNext(")", node.params[0]); 119 | } 120 | 121 | } else { 122 | if (firstToken.value === "(") { 123 | openParenToken = firstToken; 124 | closeParenToken = layout.findNext(")", node.params[0]); 125 | } 126 | } 127 | 128 | } else { 129 | 130 | openParenToken = node.async 131 | ? layout.findNext("(", firstToken) 132 | : firstToken; 133 | 134 | closeParenToken = layout.findNext(")", node.params[node.params.length - 1]); 135 | } 136 | 137 | if (openParenToken) { 138 | // have to do both in case there's a comment inside 139 | layout.noSpaceAfter(openParenToken); 140 | layout.noSpaceBefore(closeParenToken); 141 | } 142 | }, 143 | 144 | 145 | AwaitExpression(node) { 146 | const firstToken = layout.firstToken(node); 147 | layout.spaceAfter(firstToken); 148 | }, 149 | 150 | BinaryExpression(node) { 151 | const firstToken = layout.firstToken(node); 152 | const operatorToken = layout.findNext(node.operator, firstToken); 153 | layout.spaces(operatorToken); 154 | }, 155 | 156 | BlockStatement(node) { 157 | const { firstToken, lastToken } = layout.boundaryTokens(node); 158 | if (layout.isSameLine(firstToken, lastToken)) { 159 | if (node.body.length) { 160 | layout.spaceAfter(firstToken); 161 | layout.spaceBefore(lastToken); 162 | } else { 163 | layout.noSpaceAfter(firstToken); 164 | layout.noSpaceBefore(lastToken); 165 | } 166 | } 167 | }, 168 | 169 | ConditionalExpression(node) { 170 | const questionMark = layout.findPrevious("?", node.consequent); 171 | const colon = layout.findNext(":", node.consequent); 172 | 173 | layout.spaceBefore(questionMark); 174 | layout.spaces(questionMark); 175 | layout.spaces(colon); 176 | }, 177 | 178 | DoWhileStatement(node) { 179 | spaceKeywordAndBrace(node, "body", layout); 180 | 181 | const whileToken = layout.findPrevious("while", node.test); 182 | layout.spaces(whileToken); 183 | }, 184 | 185 | ExportNamedDeclaration(node) { 186 | const firstToken = layout.firstToken(node); 187 | layout.spaceAfter(firstToken); 188 | 189 | if (node.specifiers.length) { 190 | 191 | // adjust spaces around braces 192 | layout.spaceAfter(layout.findNext("{", firstToken)); 193 | layout.spaceBefore(layout.findNext("}", firstToken)); 194 | } 195 | }, 196 | 197 | ForStatement(node) { 198 | spaceKeywordAndBrace(node, "body", layout); 199 | }, 200 | 201 | ForInStatement(node) { 202 | this.ForStatement(node); 203 | }, 204 | 205 | ForOfStatement(node) { 206 | this.ForStatement(node); 207 | }, 208 | 209 | FunctionDeclaration(node, parent) { 210 | this.FunctionExpression(node, parent); 211 | }, 212 | 213 | FunctionExpression(node, parent) { 214 | 215 | // ESTree quirk: concise methods don't have "function" keyword 216 | const isConcise = 217 | (parent.type === "Property" && parent.method) || 218 | (parent.type === "MethodDefinition"); 219 | let token = layout.firstToken(node); 220 | let id, openParen; 221 | 222 | if (!isConcise) { 223 | 224 | // "async" keyword 225 | if (token.value === "async") { 226 | layout.spaceAfter(token); 227 | token = layout.nextToken(token); 228 | } 229 | 230 | // "function" keyword 231 | layout.spaceAfter(token); 232 | token = layout.nextToken(token); 233 | 234 | // "*" punctuator 235 | if (token.value === "*") { 236 | layout.noSpaceAfter(token); 237 | token = layout.nextToken(token); 238 | } 239 | 240 | // function name 241 | if (token.type === "Identifier") { 242 | layout.noSpaceAfter(token); 243 | token = layout.nextToken(token); 244 | } 245 | 246 | if (token.value === "(") { 247 | openParen = token; 248 | } else { 249 | throw new Error(`Unexpected token "${token.value}".`); 250 | } 251 | } else { 252 | let idStart = layout.firstToken(parent.key); 253 | id = idStart; 254 | 255 | if (parent.computed) { 256 | const leftBracket = layout.previousToken(idStart); 257 | layout.noSpaceAfter(leftBracket); 258 | 259 | const rightBracket = layout.nextToken(idStart); 260 | layout.noSpaceBefore(rightBracket); 261 | 262 | idStart = leftBracket; 263 | id = rightBracket; 264 | } 265 | 266 | if (parent.generator) { 267 | const star = layout.previousToken(idStart); 268 | layout.noSpaceAfter(star); 269 | } 270 | 271 | openParen = token; 272 | } 273 | 274 | if (id) { 275 | layout.noSpaceAfter(id); 276 | } 277 | 278 | layout.noSpaces(openParen); 279 | 280 | const openBrace = layout.firstToken(node.body); 281 | layout.spaceBefore(openBrace); 282 | 283 | const closeParen = layout.findPrevious(")", openBrace); 284 | layout.noSpaceBefore(closeParen); 285 | }, 286 | 287 | IfStatement(node) { 288 | spaceKeywordAndBrace(node, "consequent", layout); 289 | 290 | if (node.alternate) { 291 | const elseToken = layout.findPrevious("else", node.alternate); 292 | layout.spaces(elseToken); 293 | } 294 | }, 295 | 296 | ImportDeclaration(node) { 297 | const firstToken = layout.firstToken(node); 298 | layout.spaceAfter(firstToken); 299 | 300 | const fromToken = layout.findPrevious("from", node.source); 301 | layout.spaces(fromToken); 302 | 303 | if (node.specifiers.some(node => node.type === "ImportSpecifier")) { 304 | 305 | // adjust spaces around braces 306 | layout.spaceAfter(layout.findNext("{", firstToken)); 307 | layout.spaceBefore(layout.findNext("}", firstToken)); 308 | } 309 | }, 310 | 311 | LogicalExpression(node) { 312 | this.BinaryExpression(node); 313 | }, 314 | 315 | MethodDefinition(node) { 316 | this.FunctionExpression(node.value, node); 317 | }, 318 | 319 | ObjectExpression(node) { 320 | 321 | const { firstToken, lastToken } = layout.boundaryTokens(node); 322 | layout.spaceAfter(firstToken); 323 | 324 | if (!layout.isMultiLine(node)) { 325 | 326 | 327 | if (node.properties.length) { 328 | 329 | node.properties.forEach((property, index) => { 330 | 331 | if (index > 0) { 332 | layout.spaceBefore(property); 333 | } 334 | layout.noSpaceAfter(property); 335 | }); 336 | } 337 | } 338 | 339 | layout.spaceBefore(lastToken); 340 | }, 341 | 342 | ObjectPattern(node) { 343 | this.ObjectExpression(node); 344 | }, 345 | 346 | Property(node) { 347 | 348 | // ensure there's a space after the colon in properties 349 | if (!node.shorthand && !node.method) { 350 | 351 | layout.spaceBefore(node.value); 352 | 353 | // also be sure to check spacing of computed properties 354 | if (node.computed) { 355 | const firstToken = layout.firstToken(node.key); 356 | const openBracket = layout.findPrevious("[", firstToken); 357 | const closeBracket = layout.findNext("]", firstToken); 358 | 359 | layout.noSpaceAfter(openBracket); 360 | layout.noSpaceBefore(closeBracket); 361 | layout.noSpaceAfter(closeBracket); 362 | } else { 363 | layout.noSpaceAfter(node.key); 364 | } 365 | } 366 | 367 | if (node.method) { 368 | layout.spaceBefore(node.value.body); 369 | } 370 | }, 371 | 372 | ReturnStatement(node) { 373 | if (node.argument) { 374 | layout.spaceBefore(node.argument); 375 | } else { 376 | layout.noSpaceAfter(node); 377 | } 378 | }, 379 | 380 | SwitchStatement(node) { 381 | const firstToken = layout.firstToken(node); 382 | layout.spaceAfter(firstToken); 383 | 384 | const braceToken = layout.findNext("{", node.discriminant); 385 | layout.spaceBefore(braceToken); 386 | }, 387 | 388 | SwitchCase(node) { 389 | const colon = layout.findPrevious(":", node.consequent[0]); 390 | layout.noSpaceBefore(colon); 391 | layout.spaceAfter(colon); 392 | }, 393 | 394 | TemplateLiteral(node) { 395 | const [firstQuasi, ...quasis] = node.quasis; 396 | if (quasis.length) { 397 | layout.noSpaceAfter(firstQuasi); 398 | 399 | quasis.forEach(quasi => { 400 | layout.noSpaceBefore(quasi); 401 | layout.noSpaceAfter(quasi); 402 | }); 403 | } 404 | }, 405 | 406 | ThrowStatement(node) { 407 | const firstToken = layout.firstToken(node); 408 | layout.spaceAfter(firstToken); 409 | }, 410 | 411 | TryStatement(node) { 412 | spaceKeywordAndBrace(node, "block", layout); 413 | 414 | const catchToken = layout.firstToken(node.handler); 415 | layout.spaces(catchToken); 416 | 417 | const catchBraceToken = layout.firstToken(node.handler.body); 418 | layout.spaceBefore(catchBraceToken); 419 | 420 | if (node.finalizer) { 421 | const finallyBraceToken = layout.firstToken(node.finalizer); 422 | const finallyToken = layout.findPrevious("finally", finallyBraceToken); 423 | layout.spaces(finallyToken); 424 | } 425 | }, 426 | 427 | UpdateExpression(node) { 428 | if (node.prefix) { 429 | const operatorToken = layout.firstToken(node); 430 | 431 | // "typeof" is also an operator and requires a space no matter what 432 | if (operatorToken.type === "Punctuator") { 433 | layout.noSpaceAfter(operatorToken); 434 | } else { 435 | layout.spaceAfter(operatorToken); 436 | } 437 | } else { 438 | const operatorToken = layout.lastToken(node); 439 | layout.noSpaceBefore(operatorToken); 440 | } 441 | }, 442 | 443 | UnaryExpression(node) { 444 | this.UpdateExpression(node); 445 | }, 446 | 447 | VariableDeclaration(node) { 448 | const firstToken = layout.firstToken(node); 449 | layout.spaceAfter(firstToken); 450 | }, 451 | 452 | WhileStatement(node) { 453 | spaceKeywordAndBrace(node, "body", layout); 454 | }, 455 | 456 | YieldExpression(node) { 457 | const firstToken = layout.firstToken(node); 458 | layout.spaceAfter(firstToken); 459 | }, 460 | 461 | }; 462 | 463 | } 464 | -------------------------------------------------------------------------------- /src/util/source-code.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Wraps source code information. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { Visitor } from "../visitors.js"; 11 | 12 | //----------------------------------------------------------------------------- 13 | // Helpers 14 | //----------------------------------------------------------------------------- 15 | 16 | const parents = Symbol("parents"); 17 | 18 | //----------------------------------------------------------------------------- 19 | // Exports 20 | //----------------------------------------------------------------------------- 21 | 22 | /** 23 | * Represents all static information about source code. 24 | */ 25 | export class SourceCode { 26 | 27 | /** 28 | * Creates a new instance. 29 | * @param {string} text The source code text. 30 | * @param {string} filePath The full path to the file containing the text. 31 | * @param {Node} ast The AST representing the source code. 32 | */ 33 | constructor(text, filePath, ast) { 34 | 35 | /** 36 | * The source code text. 37 | * @property text 38 | * @type string 39 | */ 40 | this.text = text; 41 | 42 | /** 43 | * The full path to the file containing the source code. 44 | * @property filePath 45 | * @type string 46 | */ 47 | this.filePath = filePath; 48 | 49 | /** 50 | * The AST representation of the source code. 51 | * @property ast 52 | * @type Node 53 | */ 54 | this.ast = ast; 55 | 56 | /** 57 | * Map of node parents. 58 | * @property parents 59 | * @type Map 60 | * @private 61 | */ 62 | this[parents] = new Map(); 63 | 64 | // initialize the parents map 65 | const parentMap = this[parents]; 66 | const visitor = new Visitor(); 67 | visitor.visit(ast, (node, parent) => { 68 | parentMap.set(node, parent); 69 | }); 70 | } 71 | 72 | /** 73 | * Retrieves the parent of the given node. 74 | * @param {Node} node The node whose parent should be retrieved. 75 | * @returns {Node} The parent of the given node or `undefined` if node is 76 | * the root. 77 | */ 78 | getParent(node) { 79 | return this[parents].get(node); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/util/token-list.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Doubly-linked list representing tokens. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { NitpikTokenList } from "@nitpik/toolkit"; 11 | 12 | //----------------------------------------------------------------------------- 13 | // TypeDefs 14 | //----------------------------------------------------------------------------- 15 | 16 | /** 17 | * @typedef TokenListOptions 18 | * @property {boolean} collapseWhitespace If true, replaces multiple whitespace 19 | * characters with a single space. 20 | * @property {string} lineEndings The string to use as a line ending. 21 | * @property {int} maxEmptyLines The maximum number of empty lines permitted 22 | * before lines are deleted from the token list. 23 | * @property {string} quotes The string to use to quote strings. 24 | */ 25 | 26 | //----------------------------------------------------------------------------- 27 | // Private 28 | //----------------------------------------------------------------------------- 29 | 30 | const originalIndents = Symbol("originalIndents"); 31 | 32 | export const NEWLINE = /[\r\n\u2028\u2029]/; 33 | 34 | const QUOTE_ALTERNATES = new Map([ 35 | ["\"", "'"], 36 | ["`", "\""] 37 | ]); 38 | 39 | const INDENT_INCREASE_CHARS = new Set(["{", "(", "["]); 40 | const INDENT_DECREASE_CHARS = new Set(["}", ")", "]"]); 41 | 42 | /** @type TokenListOptions */ 43 | const DEFAULT_OPTIONS = { 44 | lineEndings: "\n", 45 | quotes: "\"", 46 | collapseWhitespace: true, 47 | newLinePattern: NEWLINE 48 | }; 49 | 50 | //----------------------------------------------------------------------------- 51 | // Helpers 52 | //----------------------------------------------------------------------------- 53 | /** 54 | * Converts a string token between using double and single quotes. 55 | * @param {string} value The string value to convert. 56 | * @param {string} quotes Either "double" or "single". 57 | * @returns {string} The converted string. 58 | */ 59 | function convertString(value, quotes) { 60 | 61 | // Special case: Already the correct quote style 62 | if (value.charAt(0) === quotes) { 63 | return value; 64 | } 65 | 66 | const alternate = QUOTE_ALTERNATES.get(quotes); 67 | 68 | // strip off the start and end quotes 69 | let newValue = value.slice(1, -1) 70 | 71 | // escape any instances of the desired quotes 72 | .replace(new RegExp(quotes, "g"), "\\" + quotes) 73 | 74 | // unescape any isntances of alternate quotes 75 | .replace(new RegExp(`\\\\([${alternate}])`, "g"), "$1"); 76 | 77 | // add back on the desired quotes 78 | return quotes + newValue + quotes; 79 | } 80 | 81 | function getCommentType(comment) { 82 | 83 | if (comment.type === "Line") { 84 | return "LineComment"; 85 | } 86 | 87 | if (comment.type === "Block") { 88 | return "BlockComment"; 89 | } 90 | 91 | return "HashbangComment"; 92 | } 93 | 94 | function createTokens({ tokens, comments, text }, options) { 95 | 96 | let tokenIndex = 0, commentIndex = 0; 97 | const tokensAndComments = []; 98 | 99 | while (tokenIndex < tokens.length || commentIndex < comments.length) { 100 | let comment = comments[commentIndex]; 101 | let token = tokens[tokenIndex]; 102 | 103 | // next part is a comment 104 | if (!token || (comment && comment.range[0] < token.range[0])) { 105 | tokensAndComments.push({ 106 | type: getCommentType(comment), 107 | value: text.slice(comment.range[0], comment.range[1]), 108 | range: comment.range 109 | }); 110 | commentIndex++; 111 | continue; 112 | } 113 | 114 | // next part is a token 115 | if (!comment || (token && token.range[0] < comment.range[0])) { 116 | const newToken = { 117 | type: token.type, 118 | value: token.value, 119 | range: token.range 120 | }; 121 | 122 | if (newToken.type === "String") { 123 | newToken.value = convertString(newToken.value, options.quotes); 124 | } 125 | 126 | tokensAndComments.push(newToken); 127 | tokenIndex++; 128 | continue; 129 | } 130 | 131 | } 132 | 133 | return tokensAndComments; 134 | 135 | } 136 | 137 | //----------------------------------------------------------------------------- 138 | // Exports 139 | //----------------------------------------------------------------------------- 140 | 141 | /** 142 | * A doubly-linked list representing the parts of source code. 143 | */ 144 | export class TokenList extends NitpikTokenList { 145 | 146 | /** 147 | * Creates a new instance. 148 | */ 149 | constructor(iterable = []) { 150 | 151 | super(iterable); 152 | 153 | /** 154 | * Keeps track of the original indents for some tokens. 155 | * @property originalIndents 156 | * @type Map 157 | * @private 158 | */ 159 | this[originalIndents] = new Map(); 160 | } 161 | 162 | static from({ tokens, text, options }) { 163 | 164 | const list = super.from({ tokens, text, options: { 165 | ...options, 166 | ...DEFAULT_OPTIONS 167 | }}); 168 | 169 | /* 170 | * In order to properly indent comments later on, we need to keep 171 | * track of their original indents before changes are made. 172 | */ 173 | for (const token of list) { 174 | if (list.isComment(token)) { 175 | const previousToken = list.previous(token); 176 | if (list.isIndent(previousToken)) { 177 | list[originalIndents].set(token, previousToken.value); 178 | } 179 | } 180 | } 181 | 182 | return list; 183 | } 184 | 185 | static fromAST(ast, text, options) { 186 | const finalOptions = { 187 | ...DEFAULT_OPTIONS, 188 | ...options 189 | }; 190 | 191 | const tokens = createTokens({ 192 | tokens: ast.tokens, 193 | comments: ast.comments, 194 | text 195 | }, finalOptions); 196 | 197 | return this.from({ tokens, text, finalOptions }); 198 | } 199 | 200 | /** 201 | * Returns the original indent string for a given token. 202 | * @param {Token} token The token to look up the original indent for. 203 | * @returns {string} The indent before the token in the original string or 204 | * an empty string if not found. 205 | */ 206 | getOriginalCommentIndent(token) { 207 | return this[originalIndents].get(token) || ""; 208 | } 209 | 210 | /** 211 | * Determines if a given token is a punctuator. 212 | * @param {Token} part The token to check. 213 | * @returns {boolean} True if the token is a punctuator, false if not. 214 | */ 215 | isPunctuator(part) { 216 | return part.type === "Punctuator"; 217 | } 218 | 219 | /** 220 | * Determines if a given token is a line comment. 221 | * @param {Token} part The token to check. 222 | * @returns {boolean} True if the token is a line comment, false if not. 223 | */ 224 | isLineComment(part) { 225 | return part.type === "LineComment"; 226 | } 227 | 228 | /** 229 | * Determines if a given token is a block comment. 230 | * @param {Token} part The token to check. 231 | * @returns {boolean} True if the token is a block comment, false if not. 232 | */ 233 | isBlockComment(part) { 234 | return part.type === "BlockComment"; 235 | } 236 | 237 | /** 238 | * Determines if the indent should increase after this token. 239 | * @param {Token} token The token to check. 240 | * @returns {boolean} True if the indent should be increased, false if not. 241 | */ 242 | isIndentIncreaser(token) { 243 | return (INDENT_INCREASE_CHARS.has(token.value) || this.isTemplateOpen(token)) && 244 | this.isLineBreak(this.next(token)); 245 | } 246 | 247 | /** 248 | * Determines if the indent should decrease after this token. 249 | * @param {Token} token The token to check. 250 | * @returns {boolean} True if the indent should be decreased, false if not. 251 | */ 252 | isIndentDecreaser(token) { 253 | if (INDENT_DECREASE_CHARS.has(token.value) || this.isTemplateClose(token)) { 254 | let lineBreak = this.findPreviousLineBreak(token); 255 | return !lineBreak || (this.nextToken(lineBreak) === token); 256 | } 257 | 258 | return false; 259 | } 260 | 261 | /** 262 | * Determines if a given token is part of a template literal. 263 | * @param {Token} token The token to check. 264 | * @returns {boolean} True if the token is a template, false if not. 265 | */ 266 | isTemplate(token) { 267 | return Boolean(token && token.type === "Template"); 268 | } 269 | 270 | /** 271 | * Determines if a given token is the start of a template literal with 272 | * placeholders. 273 | * @param {Token} token The token to check. 274 | * @returns {boolean} True if the token is a template start, false if not. 275 | */ 276 | isTemplateOpen(token) { 277 | return this.isTemplate(token) && token.value.endsWith("${"); 278 | } 279 | 280 | /** 281 | * Determines if a given token is the end of a template literal with 282 | * placeholders. 283 | * @param {Token} token The token to check. 284 | * @returns {boolean} True if the token is a template end, false if not. 285 | */ 286 | isTemplateClose(token) { 287 | return this.isTemplate(token) && token.value.startsWith("}"); 288 | } 289 | 290 | } 291 | -------------------------------------------------------------------------------- /src/util/wrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handles wrapping for nodes. 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Helpers 8 | //----------------------------------------------------------------------------- 9 | 10 | 11 | function shouldIncreaseIndentForVariableDeclaration(node, sourceCode) { 12 | const parent = sourceCode.getParent(node); 13 | if (parent.type === "VariableDeclarator" && parent.init === node) { 14 | const grandParent = sourceCode.getParent(parent); 15 | 16 | return grandParent.declarations.length > 1 && 17 | grandParent.declarations[0] === parent; 18 | } 19 | 20 | return false; 21 | } 22 | 23 | function unwrapObjectOrArrayLiteral(node, {layout}) { 24 | const children = node.type.startsWith("Array") ? "elements" : "properties"; 25 | const { firstToken, lastToken } = layout.boundaryTokens(node); 26 | 27 | if (node[children].length === 0) { 28 | 29 | // if there are comments then we can't unwrap 30 | if (layout.nextTokenOrComment(firstToken) === lastToken) { 31 | layout.noLineBreakAfter(firstToken); 32 | layout.noSpaceAfter(firstToken); 33 | layout.noLineBreakBefore(lastToken); 34 | layout.noSpaceBefore(lastToken); 35 | } 36 | } else { 37 | // TODO 38 | } 39 | } 40 | 41 | function wrapObjectOrArrayLiteral(node, {layout, sourceCode }) { 42 | const children = node.type.startsWith("Array") ? "elements" : "properties"; 43 | const { firstToken, lastToken } = layout.boundaryTokens(node); 44 | let originalIndentLevel = layout.getIndentLevel(node); 45 | 46 | if (shouldIncreaseIndentForVariableDeclaration(node, sourceCode)) { 47 | originalIndentLevel++; 48 | } 49 | 50 | const newIndentLevel = originalIndentLevel + 1; 51 | 52 | layout.lineBreakAfter(firstToken); 53 | layout.lineBreakBefore(lastToken); 54 | layout.indentLevel(lastToken, originalIndentLevel); 55 | 56 | if (node[children].length) { 57 | node[children].forEach(child => { 58 | 59 | const lastToken = layout.lastToken(child); 60 | const maybeComma = layout.nextToken(lastToken); 61 | 62 | if (maybeComma.value === ",") { 63 | layout.lineBreakAfter(maybeComma); 64 | } 65 | }); 66 | 67 | if (layout.options.trailingCommas) { 68 | layout.commaAfter(node[children][node[children].length - 1]); 69 | } else { 70 | layout.noCommaAfter(node[children][node[children].length - 1]); 71 | } 72 | } 73 | 74 | const firstBodyToken = layout.nextTokenOrComment(firstToken); 75 | const lastBodyToken = layout.previousTokenOrComment(lastToken); 76 | layout.indentLevelBetween(firstBodyToken, lastBodyToken, newIndentLevel); 77 | } 78 | 79 | function wrapFunction(node, { layout, sourceCode }) { 80 | const { firstToken, lastToken } = layout.boundaryTokens(node.body); 81 | const firstBodyToken = layout.nextTokenOrComment(firstToken); 82 | const lastBodyToken = layout.previousTokenOrComment(lastToken); 83 | let originalIndentLevel = layout.getIndentLevel(node); 84 | 85 | if (shouldIncreaseIndentForVariableDeclaration(node, sourceCode)) { 86 | originalIndentLevel++; 87 | } 88 | 89 | const newIndentLevel = originalIndentLevel + 1; 90 | 91 | // indent arguments 92 | if (node.params.length > 1 && layout.isLineTooLong(node)) { 93 | const openParen = layout.findPrevious("(", node.params[0]); 94 | const closeParen = layout.findPrevious(")", firstToken); 95 | 96 | layout.lineBreakAfter(openParen); 97 | layout.lineBreakBefore(closeParen); 98 | layout.indentLevel(closeParen, originalIndentLevel); 99 | 100 | node.params.forEach(param => { 101 | layout.indentLevel(param, newIndentLevel); 102 | const lastParamToken = layout.lastToken(param); 103 | const maybeComma = layout.nextToken(lastParamToken); 104 | if (maybeComma.value === ",") { 105 | layout.lineBreakAfter(maybeComma); 106 | } 107 | }); 108 | } 109 | 110 | // indent body 111 | layout.lineBreakAfter(firstToken); 112 | layout.lineBreakBefore(lastToken); 113 | layout.indentLevel(lastToken, originalIndentLevel); 114 | layout.indentLevelBetween(firstBodyToken, lastBodyToken, newIndentLevel); 115 | } 116 | 117 | function wrapBinaryOrLogicalExpression(node, { layout, sourceCode }) { 118 | const parent = sourceCode.getParent(node); 119 | const indentLevel = layout.isMultiLine(parent) 120 | ? layout.getIndentLevel(parent) + 1 121 | : layout.getIndentLevel(node) + 1; 122 | const operator = layout.findNext(node.operator, node.left); 123 | 124 | layout.lineBreakAfter(operator); 125 | layout.indentLevel(node.right, indentLevel); 126 | } 127 | 128 | function unwrapBinaryOrLogicalExpression(node, { layout }) { 129 | const operator = layout.findNext(node.operator, node.left); 130 | layout.noLineBreakAfter(operator); 131 | layout.spaces(operator); 132 | } 133 | 134 | function wrapStatementWithTestCondition(node, { layout }) { 135 | const openParen = layout.findPrevious("(", node.test); 136 | const closeParen = layout.findNext(")", node.test); 137 | 138 | layout.noLineBreakAfter(openParen); 139 | layout.lineBreakBefore(closeParen); 140 | } 141 | 142 | function unwrapStatementWithTestCondition(node, { layout }) { 143 | const openParen = layout.findPrevious("(", node.test); 144 | const closeParen = layout.findNext(")", node.test); 145 | 146 | layout.noLineBreakAfter(openParen); 147 | layout.noLineBreakBefore(closeParen); 148 | layout.noSpaceAfter(openParen); 149 | layout.noSpaceBefore(closeParen); 150 | } 151 | 152 | function wrapImportOrExport(node, layout, startSpecifierIndex = 0) { 153 | 154 | if (node.specifiers[startSpecifierIndex]) { 155 | const openBrace = layout.findPrevious("{", node.specifiers[startSpecifierIndex]); 156 | const closeBrace = layout.findNext("}", node.specifiers[node.specifiers.length - 1]); 157 | layout.lineBreakAfter(openBrace); 158 | layout.lineBreakBefore(closeBrace); 159 | 160 | for (let i = startSpecifierIndex; i < node.specifiers.length; i++) { 161 | 162 | // imports always have no indent because they are top-level 163 | layout.indentLevel(node.specifiers[i], 1); 164 | const lastSpecifierToken = layout.lastToken(node.specifiers[i]); 165 | const maybeComma = layout.nextToken(lastSpecifierToken); 166 | if (maybeComma.value === ",") { 167 | layout.noSpaceBefore(maybeComma); 168 | layout.lineBreakAfter(maybeComma); 169 | } 170 | } 171 | } 172 | 173 | } 174 | 175 | const wrappers = new Map(Object.entries({ 176 | ArrayExpression: wrapObjectOrArrayLiteral, 177 | ArrayPattern: wrapObjectOrArrayLiteral, 178 | ArrowFunctionExpression: wrapFunction, 179 | 180 | BinaryExpression: wrapBinaryOrLogicalExpression, 181 | 182 | CallExpression(node, {layout}) { 183 | const indentLevel = layout.getIndentLevel(node) + 1; 184 | const openParen = layout.findNext("(", node.callee); 185 | const closeParen = layout.lastToken(node); 186 | 187 | if (node.arguments.length > 1) { 188 | layout.lineBreakAfter(openParen); 189 | layout.lineBreakBefore(closeParen); 190 | 191 | node.arguments.forEach(argument => { 192 | layout.indentLevel(argument, indentLevel); 193 | const maybeComma = layout.nextToken(layout.lastToken(argument)); 194 | if (maybeComma.value === ",") { 195 | layout.lineBreakAfter(maybeComma); 196 | } 197 | }); 198 | 199 | layout.lineBreakBefore(closeParen); 200 | } else { 201 | layout.noSpaceAfter(openParen); 202 | layout.noSpaceBefore(closeParen); 203 | } 204 | }, 205 | 206 | ConditionalExpression(node, {layout}) { 207 | const questionMark = layout.findPrevious("?", node.consequent); 208 | const colon = layout.findNext(":", node.consequent); 209 | 210 | layout.lineBreakBefore(questionMark); 211 | layout.indent(questionMark); 212 | layout.lineBreakBefore(colon); 213 | layout.indent(colon); 214 | }, 215 | 216 | DoWhileStatement: wrapStatementWithTestCondition, 217 | 218 | ExportNamedDeclaration(node, { layout }) { 219 | wrapImportOrExport(node, layout); 220 | }, 221 | 222 | FunctionDeclaration: wrapFunction, 223 | FunctionExpression: wrapFunction, 224 | IfStatement: wrapStatementWithTestCondition, 225 | 226 | ImportDeclaration(node, { layout }) { 227 | let startSpecifierIndex = 0; 228 | 229 | // don't consider default or namespace specifiers 230 | if (node.specifiers[0].type !== "ImportSpecifier") { 231 | startSpecifierIndex = 1; 232 | } 233 | 234 | wrapImportOrExport(node, layout, startSpecifierIndex); 235 | }, 236 | 237 | LogicalExpression: wrapBinaryOrLogicalExpression, 238 | 239 | MemberExpression(node, {layout}) { 240 | 241 | // don't wrap member expressions with computed properties 242 | if (node.computed) { 243 | return; 244 | } 245 | 246 | const indentLevel = layout.getIndentLevel(node); 247 | const dot = layout.findPrevious(".", node.property); 248 | 249 | layout.lineBreakBefore(dot); 250 | layout.indentLevel(dot, indentLevel + 1); 251 | }, 252 | 253 | ObjectExpression: wrapObjectOrArrayLiteral, 254 | ObjectPattern: wrapObjectOrArrayLiteral, 255 | 256 | TemplateLiteral(node, {layout}) { 257 | const indentLevel = layout.getIndentLevel(node) + 1; 258 | node.expressions.forEach(child => { 259 | layout.lineBreakBefore(child); 260 | layout.lineBreakAfter(child); 261 | layout.indentLevel(child, indentLevel); 262 | }); 263 | }, 264 | 265 | VariableDeclaration(node, {layout}) { 266 | const indentLevel = layout.getIndentLevel(node) + 1; 267 | 268 | if (node.declarations.length > 1) { 269 | node.declarations.forEach((declarator, i) => { 270 | const lastToken = layout.lastToken(declarator); 271 | const commaToken = layout.nextToken(lastToken); 272 | if (commaToken.value === ",") { 273 | layout.lineBreakAfter(commaToken); 274 | } 275 | 276 | if (i > 0) { 277 | layout.indentLevel(declarator, indentLevel); 278 | } 279 | }); 280 | } 281 | }, 282 | WhileStatement: wrapStatementWithTestCondition, 283 | 284 | })); 285 | 286 | const unwrappers = new Map(Object.entries({ 287 | ArrayExpression: unwrapObjectOrArrayLiteral, 288 | ArrayPattern: unwrapObjectOrArrayLiteral, 289 | BinaryExpression: unwrapBinaryOrLogicalExpression, 290 | 291 | CallExpression(node, { layout }) { 292 | const openParen = layout.findNext("(", node.callee); 293 | const closeParen = layout.lastToken(node); 294 | 295 | layout.noLineBreakAfter(openParen); 296 | layout.noSpaceAfter(openParen); 297 | layout.noLineBreakBefore(closeParen); 298 | layout.noSpaceBefore(closeParen); 299 | 300 | node.arguments.forEach(argument => { 301 | const maybeComma = layout.nextToken(layout.lastToken(argument)); 302 | if (maybeComma.value === ",") { 303 | layout.noLineBreakAfter(maybeComma); 304 | layout.noSpaceBefore(maybeComma); 305 | layout.spaceAfter(maybeComma); 306 | } 307 | }); 308 | }, 309 | 310 | ConditionalExpression(node, {layout}) { 311 | const questionMark = layout.findPrevious("?", node.consequent); 312 | const colon = layout.findNext(":", node.consequent); 313 | 314 | layout.noLineBreakBefore(questionMark); 315 | layout.spaces(questionMark); 316 | layout.noLineBreakBefore(colon); 317 | layout.spaces(colon); 318 | }, 319 | 320 | DoWhileStatement: unwrapStatementWithTestCondition, 321 | IfStatement: unwrapStatementWithTestCondition, 322 | 323 | ImportDeclaration(node, { layout }) { 324 | let startSpecifierIndex = 0; 325 | 326 | // don't consider default or namespace specifiers 327 | if (node.specifiers[0].type !== "ImportSpecifier") { 328 | startSpecifierIndex = 1; 329 | } 330 | 331 | if (node.specifiers[startSpecifierIndex]) { 332 | const openBrace = layout.findPrevious("{", node.specifiers[startSpecifierIndex]); 333 | const closeBrace = layout.findNext("}", node.specifiers[node.specifiers.length - 1]); 334 | layout.noLineBreakAfter(openBrace); 335 | layout.spaceAfter(openBrace); 336 | layout.noLineBreakBefore(closeBrace); 337 | layout.spaceBefore(closeBrace); 338 | 339 | for (let i = startSpecifierIndex; i < node.specifiers.length; i++) { 340 | 341 | const lastSpecifierToken = layout.lastToken(node.specifiers[i]); 342 | const maybeComma = layout.nextToken(lastSpecifierToken); 343 | 344 | if (maybeComma.value === ",") { 345 | layout.noSpaceBefore(maybeComma); 346 | layout.noLineBreakAfter(maybeComma); 347 | layout.spaceAfter(maybeComma); 348 | } 349 | } 350 | } 351 | }, 352 | 353 | LogicalExpression: unwrapBinaryOrLogicalExpression, 354 | ObjectExpression: unwrapObjectOrArrayLiteral, 355 | ObjectPattern: unwrapObjectOrArrayLiteral, 356 | 357 | TemplateLiteral(node, {layout}) { 358 | node.expressions.forEach(child => { 359 | layout.noLineBreakBefore(child); 360 | layout.noLineBreakAfter(child); 361 | }); 362 | }, 363 | 364 | WhileStatement: unwrapStatementWithTestCondition, 365 | })); 366 | 367 | //----------------------------------------------------------------------------- 368 | // Exports 369 | //----------------------------------------------------------------------------- 370 | 371 | export class Wrapper { 372 | constructor(options) { 373 | this.options = options; 374 | } 375 | 376 | wrap(node) { 377 | return wrappers.get(node.type)(node, this.options); 378 | } 379 | 380 | unwrap(node) { 381 | return unwrappers.get(node.type)(node, this.options); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/visitors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview AST Visitors 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import estraverse from "estraverse"; 11 | import espree from "espree"; 12 | 13 | //----------------------------------------------------------------------------- 14 | // Symbols 15 | //----------------------------------------------------------------------------- 16 | 17 | const tasks = Symbol("tasks"); 18 | 19 | //----------------------------------------------------------------------------- 20 | // Visitor 21 | //----------------------------------------------------------------------------- 22 | 23 | export class Visitor { 24 | constructor(visitorKeys = espree.VisitorKeys) { 25 | this.visitorKeys = visitorKeys; 26 | } 27 | 28 | visit(ast, callback) { 29 | estraverse.traverse(ast, { 30 | enter: callback, 31 | keys: this.visitorKeys, 32 | fallback: "iteration" 33 | }); 34 | } 35 | } 36 | 37 | 38 | //----------------------------------------------------------------------------- 39 | // Task Visitor 40 | //----------------------------------------------------------------------------- 41 | 42 | export class TaskVisitor extends Visitor { 43 | constructor(visitorKeys) { 44 | super(visitorKeys); 45 | this[tasks] = []; 46 | } 47 | 48 | addTask(task) { 49 | this[tasks].push(task); 50 | } 51 | 52 | visit(ast, context) { 53 | 54 | const nodeTypes = new Map(); 55 | 56 | // create visitors 57 | this[tasks].forEach(task => { 58 | const visitor = task(context); 59 | 60 | // store node-specific visitors in a map for easy lookup 61 | Object.keys(visitor).forEach(key => { 62 | if (!Array.isArray(nodeTypes.get(key))) { 63 | nodeTypes.set(key, []); 64 | } 65 | 66 | nodeTypes.get(key).push(visitor[key].bind(visitor)); 67 | }); 68 | }); 69 | 70 | // traverse the AST 71 | super.visit(ast, (node, parent) => { 72 | const visitors = nodeTypes.get(node.type); 73 | if (visitors) { 74 | visitors.forEach(visitor => { 75 | visitor(node, parent); 76 | }); 77 | } 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/array-literals.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80, 3 | "trailingCommas": true 4 | } 5 | --- 6 | const colors = ["red" , "green" , "blue"] ; 7 | 8 | const empty = [ 9 | 10 | ]; 11 | 12 | const emptyWithComment = [ 13 | // hi 14 | ] 15 | 16 | const emptySameLine = [ ]; 17 | 18 | const numbers = [ 19 | 1, 20 | 2 21 | ]; 22 | 23 | const moreNumbers=[ 24 | 3, 25 | ]; 26 | 27 | const evenMoreNumbers = [ 28 | 3, 29 | 4, 30 | 5 31 | ]; 32 | 33 | const someReallyLongArray = [ "cody", true, "blue", 12345, "fun", 0.5, "whatever"] 34 | 35 | if (foo) { 36 | const someReallyLongArray = [ "dustin", true, "blue", 12345, "fun", 0.5, "whatever"] 37 | } 38 | --- 39 | const colors = ["red", "green", "blue"]; 40 | 41 | const empty = []; 42 | 43 | const emptyWithComment = [ 44 | // hi 45 | ]; 46 | 47 | const emptySameLine = []; 48 | 49 | const numbers = [ 50 | 1, 51 | 2, 52 | ]; 53 | 54 | const moreNumbers = [ 55 | 3, 56 | ]; 57 | 58 | const evenMoreNumbers = [ 59 | 3, 60 | 4, 61 | 5, 62 | ]; 63 | 64 | const someReallyLongArray = [ 65 | "cody", 66 | true, 67 | "blue", 68 | 12345, 69 | "fun", 70 | 0.5, 71 | "whatever", 72 | ]; 73 | 74 | if (foo) { 75 | const someReallyLongArray = [ 76 | "dustin", 77 | true, 78 | "blue", 79 | 12345, 80 | "fun", 81 | 0.5, 82 | "whatever", 83 | ]; 84 | } 85 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/classes.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | class Foo { 6 | constructor({name = "Nicholas",sport = "basketball",found = true,lost = false,count = 123}) { 7 | this.name = "foo" 8 | } 9 | 10 | static getFoo ( ) { 11 | return this; 12 | } 13 | }; 14 | 15 | class Bar extends Foo { 16 | 17 | constructor(someVariableName, someLongVariableName, someLongerVariableName, anotherVariable) { 18 | super(); 19 | } 20 | 21 | *values ( ) { 22 | return [ ]; 23 | } 24 | 25 | [ baz ] ( b ){ 26 | return b; 27 | } 28 | 29 | async fetch (a){ 30 | await foo(a) 31 | } 32 | } 33 | --- 34 | class Foo { 35 | constructor({ 36 | name = "Nicholas", 37 | sport = "basketball", 38 | found = true, 39 | lost = false, 40 | count = 123 41 | }) { 42 | this.name = "foo"; 43 | } 44 | 45 | static getFoo() { 46 | return this; 47 | } 48 | } 49 | 50 | class Bar extends Foo { 51 | 52 | constructor( 53 | someVariableName, 54 | someLongVariableName, 55 | someLongerVariableName, 56 | anotherVariable 57 | ) { 58 | super(); 59 | } 60 | 61 | *values() { 62 | return []; 63 | } 64 | 65 | [baz](b) { 66 | return b; 67 | } 68 | 69 | async fetch(a) { 70 | await foo(a); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/comments.txt: -------------------------------------------------------------------------------- 1 | {} 2 | --- 3 | const x = { 4 | foo: "bar" 5 | }; 6 | 7 | /** 8 | * @fileoverview Main application object for a CLI. 9 | * @author Nicholas C. Zakas 10 | */ 11 | 12 | /** 13 | * JSDoc 14 | * @param {string} foo 15 | * @returns {string} something. 16 | */ 17 | 18 | /* block comment */ 19 | // line comment 20 | /* block 21 | comment 2 */ 22 | 23 | if (foo) { 24 | bar(); 25 | // line comment 2 26 | 27 | /* 28 | * Block comment 3 29 | */ 30 | while (something) { 31 | /* 32 | block comment 4 33 | */ 34 | 35 | /* 36 | block comment 5 37 | */ 38 | } 39 | } 40 | --- 41 | const x = { 42 | foo: "bar" 43 | }; 44 | 45 | /** 46 | * @fileoverview Main application object for a CLI. 47 | * @author Nicholas C. Zakas 48 | */ 49 | 50 | /** 51 | * JSDoc 52 | * @param {string} foo 53 | * @returns {string} something. 54 | */ 55 | 56 | /* block comment */ 57 | // line comment 58 | /* block 59 | comment 2 */ 60 | 61 | if (foo) { 62 | bar(); 63 | // line comment 2 64 | 65 | /* 66 | * Block comment 3 67 | */ 68 | while (something) { 69 | /* 70 | block comment 4 71 | */ 72 | 73 | /* 74 | block comment 5 75 | */ 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/config.txt: -------------------------------------------------------------------------------- 1 | { 2 | "quotes": "single", 3 | "indent": "\t", 4 | "maxEmptyLines": 1 5 | } 6 | --- 7 | module.exports = [ 8 | 9 | 10 | { 11 | files: ["nitpik.config.js"], 12 | formatter: new JavaScriptFormatter({ 13 | style: { 14 | quotes: "single", 15 | indent: "\t" 16 | } 17 | }) 18 | } 19 | 20 | 21 | ]; 22 | --- 23 | module.exports = [ 24 | 25 | { 26 | files: ['nitpik.config.js'], 27 | formatter: new JavaScriptFormatter({ 28 | style: { 29 | quotes: 'single', 30 | indent: '\t' 31 | } 32 | }) 33 | } 34 | 35 | ]; 36 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/destructuring.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80, 3 | "trailingCommas": true 4 | } 5 | --- 6 | let [foo,bar]= baz; 7 | 8 | const [ someReallyLongVariableName, anotherReallyLongVariableName, ...somethingElse ] = foo; 9 | 10 | const {firstToken , lastToken}=foo; 11 | 12 | const { firstToken: firstLongVariableName, lastToken:lastLongVariableName } = foo; 13 | 14 | if (foo) { 15 | let { firstToken: firstLongVariableName, lastToken:lastLongVariableName } = foo; 16 | } 17 | --- 18 | let [foo, bar] = baz; 19 | 20 | const [ 21 | someReallyLongVariableName, 22 | anotherReallyLongVariableName, 23 | ...somethingElse, 24 | ] = foo; 25 | 26 | const { firstToken, lastToken } = foo; 27 | 28 | const { 29 | firstToken: firstLongVariableName, 30 | lastToken: lastLongVariableName, 31 | } = foo; 32 | 33 | if (foo) { 34 | let { 35 | firstToken: firstLongVariableName, 36 | lastToken: lastLongVariableName, 37 | } = foo; 38 | } 39 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/empty-statement.txt: -------------------------------------------------------------------------------- 1 | {} 2 | --- 3 | function foo() { 4 | // todo 5 | }; 6 | 7 | if (foo); 8 | 9 | while (foo); 10 | 11 | for (a;b;c); 12 | 13 | for (const a of b); 14 | 15 | for (const c in d); 16 | --- 17 | function foo() { 18 | // todo 19 | } 20 | 21 | if (foo); 22 | 23 | while (foo); 24 | 25 | for (a; b; c); 26 | 27 | for (const a of b); 28 | 29 | for (const c in d); 30 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/exports.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | let bar= 6; 6 | const baz =89 7 | const bang= "defined"; 8 | export const foo=5 9 | export default function() {}; 10 | export {bar}; 11 | export {baz ,bang} 12 | export { bing} from "bar" ; 13 | 14 | export { someReallyLongVariableName as v, anotherReallyLongVariableName, yetAnotherReallyLongVariableName as zz } from "somewhere"; 15 | --- 16 | let bar = 6; 17 | const baz = 89; 18 | const bang = "defined"; 19 | export const foo = 5; 20 | export default function() {} 21 | export { bar }; 22 | export { baz, bang }; 23 | export { bing } from "bar"; 24 | 25 | export { 26 | someReallyLongVariableName as v, 27 | anotherReallyLongVariableName, 28 | yetAnotherReallyLongVariableName as zz 29 | } from "somewhere"; 30 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/expressions.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | somethingLongerThanNecessary ? anotherLongerThanNecessaryThing : andOneMoreLongThing; 6 | a?b:c 7 | f(( )=>{}) 8 | f(async( /* foo */ )=>{}) 9 | f(( a,b )=>{}) 10 | f(a=>b); 11 | 12 | somethingLongerThanNecessary.anotherLongerThanNecessaryThing.andOneMoreLongThing; 13 | 14 | let x =somethingLongerThanNecessary.anotherLongerThanNecessaryThing().andOneMoreLongThing(); 15 | 16 | foo 17 | ? bar 18 | : baz; 19 | 20 | a . b . c; 21 | 22 | foo 23 | .bar 24 | .baz 25 | .boom(); 26 | 27 | someLongFunctionName(somethingLongerThanNecessary,anotherLongerThanNecessaryThing,andOneMoreLongThing); 28 | 29 | somethingLongerThanNecessary + anotherLongerThanNecessaryThing + andOneMoreLongThing; 30 | 31 | somethingLongerThanNecessary || anotherLongerThanNecessaryThing && andOneMoreLongThing; 32 | --- 33 | somethingLongerThanNecessary 34 | ? anotherLongerThanNecessaryThing 35 | : andOneMoreLongThing; 36 | a ? b : c; 37 | f(() => {}); 38 | f(async (/* foo */) => {}); 39 | f((a, b) => {}); 40 | f(a => b); 41 | 42 | somethingLongerThanNecessary 43 | .anotherLongerThanNecessaryThing 44 | .andOneMoreLongThing; 45 | 46 | let x = somethingLongerThanNecessary 47 | .anotherLongerThanNecessaryThing() 48 | .andOneMoreLongThing(); 49 | 50 | foo 51 | ? bar 52 | : baz; 53 | 54 | a.b.c; 55 | 56 | foo 57 | .bar 58 | .baz 59 | .boom(); 60 | 61 | someLongFunctionName( 62 | somethingLongerThanNecessary, 63 | anotherLongerThanNecessaryThing, 64 | andOneMoreLongThing 65 | ); 66 | 67 | somethingLongerThanNecessary + 68 | anotherLongerThanNecessaryThing + 69 | andOneMoreLongThing; 70 | 71 | somethingLongerThanNecessary || 72 | anotherLongerThanNecessaryThing && 73 | andOneMoreLongThing; 74 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/functions.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | it("some title", () => { 6 | console.log("Hi"); 7 | }); 8 | 9 | function doSomething({greeting = "hello", greeted = '"World"', silent = false, onMouseOver}) { 10 | 11 | if(!greeting){return null}; 12 | } 13 | 14 | function doSomethingElse(greeting = "hello", greeted = "world", silent = false, onMouseOver) { 15 | return true; 16 | } 17 | 18 | var x = function({greeting = "hello", greeted = "world"}, silent = false, onMouseOver) { 19 | return true; 20 | } 21 | 22 | const y = function({greeting = "hello", greeted = "world", silent = false, onMouseOver, someReallyLongVariable}, foo) { 23 | return true; 24 | }, z = "hi"; 25 | 26 | const a = ({greeting = "hello", greeted = "world", silent = false, onMouseOver, someReallyLongVariable}, foo) => { 27 | return true; 28 | }; 29 | 30 | call( 31 | "some title", 32 | () => { 33 | console.log("Hi"); 34 | }); 35 | --- 36 | it("some title", () => { 37 | console.log("Hi"); 38 | }); 39 | 40 | function doSomething({ 41 | greeting = "hello", 42 | greeted = "\"World\"", 43 | silent = false, 44 | onMouseOver 45 | }) { 46 | 47 | if (!greeting) { return null; } 48 | } 49 | 50 | function doSomethingElse( 51 | greeting = "hello", 52 | greeted = "world", 53 | silent = false, 54 | onMouseOver 55 | ) { 56 | return true; 57 | } 58 | 59 | var x = function( 60 | { greeting = "hello", greeted = "world" }, 61 | silent = false, 62 | onMouseOver 63 | ) { 64 | return true; 65 | }; 66 | 67 | const y = function( 68 | { 69 | greeting = "hello", 70 | greeted = "world", 71 | silent = false, 72 | onMouseOver, 73 | someReallyLongVariable 74 | }, 75 | foo 76 | ) { 77 | return true; 78 | }, 79 | z = "hi"; 80 | 81 | const a = ( 82 | { 83 | greeting = "hello", 84 | greeted = "world", 85 | silent = false, 86 | onMouseOver, 87 | someReallyLongVariable 88 | }, 89 | foo 90 | ) => { 91 | return true; 92 | }; 93 | 94 | call( 95 | "some title", 96 | () => { 97 | console.log("Hi"); 98 | } 99 | ); 100 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/hashbang.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | #!/usr/bin/env node 6 | 7 | 8 | let bar= 6; 9 | --- 10 | #!/usr/bin/env node 11 | 12 | let bar = 6; 13 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/imports.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | import {foo} from "bar" 6 | import {bar,baz} from "bang"; 7 | import * as bang from "bing" 8 | import "yo" 9 | 10 | import {aReallyLongVariableName, anotherReallyLongVariableName, yetAnotherReallyLongVariableName} from "foo"; 11 | 12 | import bing, {aReallyLongVariableName2, anotherReallyLongVariableName2, yetAnotherReallyLongVariableName2} from "foo"; 13 | --- 14 | import { foo } from "bar"; 15 | import { bar, baz } from "bang"; 16 | import * as bang from "bing"; 17 | import "yo"; 18 | 19 | import { 20 | aReallyLongVariableName, 21 | anotherReallyLongVariableName, 22 | yetAnotherReallyLongVariableName 23 | } from "foo"; 24 | 25 | import bing, { 26 | aReallyLongVariableName2, 27 | anotherReallyLongVariableName2, 28 | yetAnotherReallyLongVariableName2 29 | } from "foo"; 30 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/keywords.txt: -------------------------------------------------------------------------------- 1 | {} 2 | --- 3 | import{bar}from"baz"; 4 | export{bar}; 5 | 6 | if(foo){ 7 | bar(); 8 | }else{ 9 | baz(); 10 | } 11 | 12 | while(foo){ 13 | bar(); 14 | } 15 | 16 | for(let i=0;i<10;i ++) { 17 | bar(i); 18 | } 19 | 20 | do{ 21 | bar(); 22 | }while(baz); 23 | 24 | for(x in b) { 25 | bar(); 26 | } 27 | 28 | for(x of b) { 29 | bar(); 30 | } 31 | 32 | function bang() { 33 | bar(); 34 | } 35 | 36 | async function bing() { 37 | return bar(); 38 | } 39 | 40 | switch(foo){ 41 | case "a" : b(); 42 | default : c(); 43 | } 44 | 45 | try{ 46 | bar() 47 | }catch(ex){ 48 | baz(); 49 | }finally{ 50 | // whatever 51 | } 52 | 53 | throw (foo); 54 | throw(bar); 55 | 56 | function *gen (){ 57 | yield(5); 58 | } 59 | 60 | function *gen2 (){ 61 | yield 5; 62 | } 63 | 64 | async function fetch () { 65 | await(5); 66 | } 67 | 68 | async function fetch2 () { 69 | await 5; 70 | } 71 | --- 72 | import { bar } from "baz"; 73 | export { bar }; 74 | 75 | if (foo) { 76 | bar(); 77 | } else { 78 | baz(); 79 | } 80 | 81 | while (foo) { 82 | bar(); 83 | } 84 | 85 | for (let i = 0; i < 10; i++) { 86 | bar(i); 87 | } 88 | 89 | do { 90 | bar(); 91 | } while (baz); 92 | 93 | for (x in b) { 94 | bar(); 95 | } 96 | 97 | for (x of b) { 98 | bar(); 99 | } 100 | 101 | function bang() { 102 | bar(); 103 | } 104 | 105 | async function bing() { 106 | return bar(); 107 | } 108 | 109 | switch (foo) { 110 | case "a": b(); 111 | default: c(); 112 | } 113 | 114 | try { 115 | bar(); 116 | } catch (ex) { 117 | baz(); 118 | } finally { 119 | // whatever 120 | } 121 | 122 | throw (foo); 123 | throw (bar); 124 | 125 | function *gen() { 126 | yield (5); 127 | } 128 | 129 | function *gen2() { 130 | yield 5; 131 | } 132 | 133 | async function fetch() { 134 | await (5); 135 | } 136 | 137 | async function fetch2() { 138 | await 5; 139 | } 140 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/multiline-function-call.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80, 3 | "trailingCommas": true, 4 | "indent": "\t" 5 | } 6 | --- 7 | function foo() { 8 | return Promise.all( 9 | filePaths.map(filePath => this.formatFile(filePath)) 10 | ); 11 | } 12 | --- 13 | function foo() { 14 | return Promise.all( 15 | filePaths.map(filePath => this.formatFile(filePath)) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/object-literals.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80, 3 | "maxEmptyLines": 2, 4 | "trailingCommas": true 5 | } 6 | --- 7 | module.exports = { 8 | colors :[ "red" ,"green" ,"blue" ], 9 | name :'esfmt', 10 | doSomething({ name= "Nicholas", sport= "basketball", found=true, lost=false, count=123}){ 11 | // some comment 12 | return 'I said, "hi!"' 13 | }, 14 | [ again ] ( ) { 15 | return "yo" 16 | }, 17 | } 18 | --- 19 | module.exports = { 20 | colors: ["red", "green", "blue"], 21 | name: "esfmt", 22 | doSomething({ 23 | name = "Nicholas", 24 | sport = "basketball", 25 | found = true, 26 | lost = false, 27 | count = 123, 28 | }) { 29 | // some comment 30 | return "I said, \"hi!\""; 31 | }, 32 | [again]() { 33 | return "yo"; 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/operators.txt: -------------------------------------------------------------------------------- 1 | {} 2 | --- 3 | + 1 4 | 1 +- 2; 5 | 1+1; 6 | let result = 5/ 6; 7 | ++ a; 8 | 1+ -- b; 9 | a*b; 10 | a **b; 11 | a ||b&&c 12 | a|b 13 | a^b; 14 | b ++; 15 | a%b 16 | typeof value === "string" 17 | foo instanceof bar 18 | --- 19 | +1; 20 | 1 + -2; 21 | 1 + 1; 22 | let result = 5 / 6; 23 | ++a; 24 | 1 + --b; 25 | a * b; 26 | a ** b; 27 | a || b && c; 28 | a | b; 29 | a ^ b; 30 | b++; 31 | a % b; 32 | typeof value === "string"; 33 | foo instanceof bar; 34 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/statements.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | if (foo) 6 | bar() 7 | 8 | while(foo) 9 | bar(); 10 | 11 | for (const foo of bar) 12 | baz() 13 | 14 | for (const foo in bar) 15 | baz() 16 | 17 | for (let i=0;i<10;i++) 18 | bar() 19 | 20 | if (somethingLongerThanNecessary || anotherLongerThanNecessaryThing && andOneMoreLongThing) { 21 | foo(); 22 | } 23 | 24 | while (somethingLongerThanNecessary || anotherLongerThanNecessaryThing && andOneMoreLongThing) { 25 | foo(); 26 | } 27 | 28 | do { 29 | foo(); 30 | } while (somethingLongerThanNecessary || anotherLongerThanNecessaryThing && andOneMoreLongThing); 31 | --- 32 | if (foo) 33 | bar(); 34 | 35 | while (foo) 36 | bar(); 37 | 38 | for (const foo of bar) 39 | baz(); 40 | 41 | for (const foo in bar) 42 | baz(); 43 | 44 | for (let i = 0; i < 10; i++) 45 | bar(); 46 | 47 | if (somethingLongerThanNecessary || 48 | anotherLongerThanNecessaryThing && 49 | andOneMoreLongThing 50 | ) { 51 | foo(); 52 | } 53 | 54 | while (somethingLongerThanNecessary || 55 | anotherLongerThanNecessaryThing && 56 | andOneMoreLongThing 57 | ) { 58 | foo(); 59 | } 60 | 61 | do { 62 | foo(); 63 | } while (somethingLongerThanNecessary || 64 | anotherLongerThanNecessaryThing && 65 | andOneMoreLongThing 66 | ); 67 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/strings.txt: -------------------------------------------------------------------------------- 1 | {} 2 | --- 3 | const greeting = 'Hello world!'; 4 | const line = 'She didn\'t say, "hi."'; 5 | --- 6 | const greeting = "Hello world!"; 7 | const line = "She didn't say, \"hi.\""; 8 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/switch-statement.txt: -------------------------------------------------------------------------------- 1 | {} 2 | --- 3 | switch(foo){ 4 | case "bar" : 5 | hello(); 6 | break 7 | 8 | case "baz": 9 | world(); 10 | break; 11 | 12 | 13 | 14 | case "bang": { 15 | yay() 16 | } 17 | 18 | 19 | default: whatever(); 20 | }; 21 | --- 22 | switch (foo) { 23 | case "bar": 24 | hello(); 25 | break; 26 | 27 | case "baz": 28 | world(); 29 | break; 30 | 31 | case "bang": { 32 | yay(); 33 | } 34 | 35 | default: whatever(); 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/template-strings.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | const a = `Hello world`; 6 | b(`hi ${ name } there`) 7 | 8 | c(`what 9 | is 10 | ${word}?`) 11 | 12 | d(`what 13 | is 14 | ${ 15 | 16 | 17 | word 18 | }?` ); 19 | 20 | e(`what 21 | is 22 | ${ 23 | word 24 | /*whatever*/}?` ); 25 | 26 | f`something ${ 27 | word 28 | } else`; 29 | 30 | const longTemplateString = `first part ${ part } second part ${ part2 } third part ${ part4 } fourth part`; 31 | --- 32 | const a = `Hello world`; 33 | b(`hi ${name} there`); 34 | 35 | c(`what 36 | is 37 | ${word}?`); 38 | 39 | d(`what 40 | is 41 | ${ 42 | 43 | word 44 | }?`); 45 | 46 | e(`what 47 | is 48 | ${ 49 | word 50 | /*whatever*/}?`); 51 | 52 | f`something ${ 53 | word 54 | } else`; 55 | 56 | const longTemplateString = `first part ${ 57 | part 58 | } second part ${ 59 | part2 60 | } third part ${ 61 | part4 62 | } fourth part`; 63 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/trailing-whitespace.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80, 3 | "trailingCommas": true, 4 | "indent": "\t" 5 | } 6 | --- 7 | node.forEach(inputFile => { 8 | inputFile = "packages/core/" + inputFile; 9 | }); 10 | 11 | if (foo) { 12 | bar(); 13 | } 14 | --- 15 | node.forEach(inputFile => { 16 | inputFile = "packages/core/" + inputFile; 17 | }); 18 | 19 | if (foo) { 20 | bar(); 21 | } 22 | -------------------------------------------------------------------------------- /tests/fixtures/formatter/variable-declarations.txt: -------------------------------------------------------------------------------- 1 | { 2 | "maxLineLength": 80 3 | } 4 | --- 5 | var foo,bar,baz; 6 | 7 | let someReallyLongVariableName, anotherReallyLongVariableName, somethingElseReallyLong; 8 | 9 | // functions 10 | 11 | const yy = function () { 12 | bar(); 13 | }; 14 | 15 | const x = function () { 16 | // hello 17 | bar(); 18 | },y=5; 19 | 20 | const a = 5, 21 | b = function() { 22 | bar(); 23 | }, 24 | c = 5; 25 | 26 | const yya = ()=> { 27 | bar(); 28 | }; 29 | 30 | const xa = () =>{ 31 | // hello 32 | bar(); 33 | },ya=5; 34 | 35 | const aa = 5, 36 | ba = () =>{ 37 | bar(); 38 | }, 39 | ca = 5; 40 | 41 | // object literals 42 | 43 | var person = { 44 | name:"Nicholas" 45 | } 46 | 47 | let task = { 48 | id: 1 49 | }, num=2; 50 | 51 | let count= 3, 52 | task2 = { 53 | // test 54 | id: 1, 55 | completed: false 56 | }; 57 | 58 | // array literals 59 | 60 | let items = [ 61 | "foo" 62 | ]; 63 | 64 | let items2 = [ 65 | "foo" 66 | ], item2Count=1; 67 | 68 | let items3Count=2, 69 | items3 = [ 70 | // hi 71 | "foo", "bar" 72 | ]; 73 | --- 74 | var foo, bar, baz; 75 | 76 | let someReallyLongVariableName, 77 | anotherReallyLongVariableName, 78 | somethingElseReallyLong; 79 | 80 | // functions 81 | 82 | const yy = function() { 83 | bar(); 84 | }; 85 | 86 | const x = function() { 87 | // hello 88 | bar(); 89 | }, 90 | y = 5; 91 | 92 | const a = 5, 93 | b = function() { 94 | bar(); 95 | }, 96 | c = 5; 97 | 98 | const yya = () => { 99 | bar(); 100 | }; 101 | 102 | const xa = () => { 103 | // hello 104 | bar(); 105 | }, 106 | ya = 5; 107 | 108 | const aa = 5, 109 | ba = () => { 110 | bar(); 111 | }, 112 | ca = 5; 113 | 114 | // object literals 115 | 116 | var person = { 117 | name: "Nicholas" 118 | }; 119 | 120 | let task = { 121 | id: 1 122 | }, 123 | num = 2; 124 | 125 | let count = 3, 126 | task2 = { 127 | // test 128 | id: 1, 129 | completed: false 130 | }; 131 | 132 | // array literals 133 | 134 | let items = [ 135 | "foo" 136 | ]; 137 | 138 | let items2 = [ 139 | "foo" 140 | ], 141 | item2Count = 1; 142 | 143 | let items3Count = 2, 144 | items3 = [ 145 | // hi 146 | "foo", 147 | "bar" 148 | ]; 149 | -------------------------------------------------------------------------------- /tests/fixtures/raw/config.txt: -------------------------------------------------------------------------------- 1 | node.forEach(inputFile => { 2 | inputFile.output.file = "packages/core/" + inputFile.output.file; 3 | }); 4 | -------------------------------------------------------------------------------- /tests/fixtures/token-list/conditional-multiline.txt: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | --- 4 | foo 5 | ? bar 6 | : baz; 7 | --- 8 | [ 9 | { 10 | "type": "Identifier", 11 | "value": "foo", 12 | "range": [ 0, 3 ] 13 | }, 14 | { "type": "LineBreak", "value": "\n", "range": [ 3, 3 ] }, 15 | { "type": "Whitespace", "value": " ", "range": [ 4, 8 ] }, 16 | { "type": "Punctuator", "value": "?", "range": [ 8, 9 ] }, 17 | { "type": "Whitespace", "value": " ", "range": [ 9, 10 ] }, 18 | { 19 | "type": "Identifier", 20 | "value": "bar", 21 | "range": [ 10, 13 ] 22 | }, 23 | { "type": "LineBreak", "value": "\n", "range": [ 13, 13 ] }, 24 | { "type": "Whitespace", "value": " ", "range": [ 14, 18 ] }, 25 | { 26 | "type": "Punctuator", 27 | "value": ":", 28 | "range": [ 18, 19 ] 29 | }, 30 | { "type": "Whitespace", "value": " ", "range": [ 19, 20 ] }, 31 | { 32 | "type": "Identifier", 33 | "value": "baz", 34 | "range": [ 20, 23 ] 35 | }, 36 | { 37 | "type": "Punctuator", 38 | "value": ";", 39 | "range": [ 23, 24 ] 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /tests/fixtures/token-list/empty-line-whitespace.txt: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | --- 4 | for (const foo of bar) 5 | baz() 6 | 7 | for (const foo in bar) 8 | baz() 9 | --- 10 | [ 11 | { 12 | "type": "Keyword", 13 | "value": "for", 14 | "range": [ 15 | 0, 16 | 3 17 | ] 18 | }, 19 | { 20 | "type": "Whitespace", 21 | "value": " ", 22 | "range": [ 23 | 3, 24 | 4 25 | ] 26 | }, 27 | { 28 | "type": "Punctuator", 29 | "value": "(", 30 | "range": [ 31 | 4, 32 | 5 33 | ] 34 | }, 35 | { 36 | "type": "Keyword", 37 | "value": "const", 38 | "range": [ 39 | 5, 40 | 10 41 | ] 42 | }, 43 | { 44 | "type": "Whitespace", 45 | "value": " ", 46 | "range": [ 47 | 10, 48 | 11 49 | ] 50 | }, 51 | { 52 | "type": "Identifier", 53 | "value": "foo", 54 | "range": [ 55 | 11, 56 | 14 57 | ] 58 | }, 59 | { 60 | "type": "Whitespace", 61 | "value": " ", 62 | "range": [ 63 | 14, 64 | 15 65 | ] 66 | }, 67 | { 68 | "type": "Identifier", 69 | "value": "of", 70 | "range": [ 71 | 15, 72 | 17 73 | ] 74 | }, 75 | { 76 | "type": "Whitespace", 77 | "value": " ", 78 | "range": [ 79 | 17, 80 | 18 81 | ] 82 | }, 83 | { 84 | "type": "Identifier", 85 | "value": "bar", 86 | "range": [ 87 | 18, 88 | 21 89 | ] 90 | }, 91 | { 92 | "type": "Punctuator", 93 | "value": ")", 94 | "range": [ 95 | 21, 96 | 22 97 | ] 98 | }, 99 | { 100 | "type": "LineBreak", 101 | "value": "\n", 102 | "range": [ 103 | 22, 104 | 22 105 | ] 106 | }, 107 | { 108 | "type": "Identifier", 109 | "value": "baz", 110 | "range": [ 111 | 23, 112 | 26 113 | ] 114 | }, 115 | { 116 | "type": "Punctuator", 117 | "value": "(", 118 | "range": [ 119 | 26, 120 | 27 121 | ] 122 | }, 123 | { 124 | "type": "Punctuator", 125 | "value": ")", 126 | "range": [ 127 | 27, 128 | 28 129 | ] 130 | }, 131 | { 132 | "type": "LineBreak", 133 | "value": "\n", 134 | "range": [ 135 | 28, 136 | 28 137 | ] 138 | }, 139 | { 140 | "type": "Whitespace", 141 | "value": " ", 142 | "range": [ 143 | 29, 144 | 33 145 | ] 146 | }, 147 | { 148 | "type": "LineBreak", 149 | "value": "\n", 150 | "range": [ 151 | 33, 152 | 33 153 | ] 154 | }, 155 | { 156 | "type": "Keyword", 157 | "value": "for", 158 | "range": [ 159 | 34, 160 | 37 161 | ] 162 | }, 163 | { 164 | "type": "Whitespace", 165 | "value": " ", 166 | "range": [ 167 | 37, 168 | 38 169 | ] 170 | }, 171 | { 172 | "type": "Punctuator", 173 | "value": "(", 174 | "range": [ 175 | 38, 176 | 39 177 | ] 178 | }, 179 | { 180 | "type": "Keyword", 181 | "value": "const", 182 | "range": [ 183 | 39, 184 | 44 185 | ] 186 | }, 187 | { 188 | "type": "Whitespace", 189 | "value": " ", 190 | "range": [ 191 | 44, 192 | 45 193 | ] 194 | }, 195 | { 196 | "type": "Identifier", 197 | "value": "foo", 198 | "range": [ 199 | 45, 200 | 48 201 | ] 202 | }, 203 | { 204 | "type": "Whitespace", 205 | "value": " ", 206 | "range": [ 207 | 48, 208 | 49 209 | ] 210 | }, 211 | { 212 | "type": "Keyword", 213 | "value": "in", 214 | "range": [ 215 | 49, 216 | 51 217 | ] 218 | }, 219 | { 220 | "type": "Whitespace", 221 | "value": " ", 222 | "range": [ 223 | 51, 224 | 52 225 | ] 226 | }, 227 | { 228 | "type": "Identifier", 229 | "value": "bar", 230 | "range": [ 231 | 52, 232 | 55 233 | ] 234 | }, 235 | { 236 | "type": "Punctuator", 237 | "value": ")", 238 | "range": [ 239 | 55, 240 | 56 241 | ] 242 | }, 243 | { 244 | "type": "LineBreak", 245 | "value": "\n", 246 | "range": [ 247 | 56, 248 | 56 249 | ] 250 | }, 251 | { 252 | "type": "Identifier", 253 | "value": "baz", 254 | "range": [ 255 | 57, 256 | 60 257 | ] 258 | }, 259 | { 260 | "type": "Punctuator", 261 | "value": "(", 262 | "range": [ 263 | 60, 264 | 61 265 | ] 266 | }, 267 | { 268 | "type": "Punctuator", 269 | "value": ")", 270 | "range": [ 271 | 61, 272 | 62 273 | ] 274 | } 275 | ] 276 | -------------------------------------------------------------------------------- /tests/fixtures/token-list/template-string-leading-whitespace.txt: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | --- 4 | `start`; 5 | --- 6 | [ 7 | { 8 | "type": "Whitespace", 9 | "value": " ", 10 | "range": [ 11 | 0, 12 | 4 13 | ] 14 | }, 15 | { 16 | "type": "Template", 17 | "value": "`start`", 18 | "range": [ 19 | 4, 20 | 11 21 | ] 22 | }, 23 | { 24 | "type": "Punctuator", 25 | "value": ";", 26 | "range": [ 27 | 11, 28 | 12 29 | ] 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /tests/formatter.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for formatter 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { Formatter } from "../src/formatter.js"; 11 | import fs from "fs"; 12 | import path from "path"; 13 | import chai from "chai"; 14 | 15 | const expect = chai.expect; 16 | 17 | //----------------------------------------------------------------------------- 18 | // Formatter Configs 19 | //----------------------------------------------------------------------------- 20 | 21 | 22 | //----------------------------------------------------------------------------- 23 | // Tests 24 | //----------------------------------------------------------------------------- 25 | 26 | describe("Formatter", () => { 27 | 28 | describe("Plugins", () => { 29 | 30 | it("should run plugin when specified", () => { 31 | 32 | const formatter = new Formatter({ 33 | options: { 34 | maxEmptyLines: 2 35 | }, 36 | plugins: [ 37 | 38 | // insert a line break at end of input 39 | function(context) { 40 | return { 41 | ExpressionStatement(node) { 42 | const last = context.layout.lastToken(node); 43 | const semi = context.layout.nextToken(last); 44 | context.layout.lineBreakAfter(semi); 45 | } 46 | }; 47 | } 48 | ] 49 | }); 50 | 51 | const result = formatter.format("a;"); 52 | expect(result).to.deep.equal("a;\n"); 53 | }); 54 | 55 | it("should run multiple plugins when specified", () => { 56 | 57 | const formatter = new Formatter({ 58 | options: { 59 | maxEmptyLines: 2 60 | }, 61 | plugins: [ 62 | 63 | // insert a line break at end of input 64 | function(context) { 65 | return { 66 | ExpressionStatement(node) { 67 | const last = context.layout.lastToken(node); 68 | const semi = context.layout.nextToken(last); 69 | context.layout.lineBreakAfter(semi); 70 | } 71 | }; 72 | }, 73 | 74 | // ensure empty line before function declarations 75 | function(context) { 76 | 77 | const { layout } = context; 78 | 79 | return { 80 | FunctionDeclaration(node) { 81 | layout.emptyLineBefore(node); 82 | } 83 | }; 84 | } 85 | ] 86 | }); 87 | 88 | const result = formatter.format("function foo(){\nreturn;}"); 89 | expect(result).to.deep.equal("\nfunction foo() {\n return;\n}\n"); 90 | }); 91 | 92 | it("should not run plugins when plugin array is empty", () => { 93 | 94 | const formatter = new Formatter({ 95 | style: { 96 | maxEmptyLines: 2, 97 | emptyLastLine: false 98 | }, 99 | plugins: [] 100 | }); 101 | 102 | const result = formatter.format("a;"); 103 | expect(result).to.deep.equal("a;"); 104 | }); 105 | 106 | }); 107 | 108 | describe("Style Options", () => { 109 | 110 | describe("semicolons", () => { 111 | it("should not add semicolons when semicolons is false", () => { 112 | const source = "a\nb"; 113 | const expected = "a\nb\n"; 114 | const formatter = new Formatter({ 115 | style: { 116 | semicolons: false 117 | } 118 | }); 119 | const result = formatter.format(source); 120 | expect(result).to.deep.equal(expected); 121 | 122 | }); 123 | 124 | it("should remove semicolons when semicolons is false and semicolons are present", () => { 125 | const source = "a;\nb;"; 126 | const expected = "a\nb\n"; 127 | const formatter = new Formatter({ 128 | style: { 129 | semicolons: false 130 | } 131 | }); 132 | const result = formatter.format(source); 133 | expect(result).to.deep.equal(expected); 134 | 135 | }); 136 | 137 | it("should not remove semicolons when semicolons is false and semicolon is not followed by a line break", () => { 138 | const source = "a;b;"; 139 | const expected = "a; b\n"; 140 | const formatter = new Formatter({ 141 | style: { 142 | semicolons: false 143 | } 144 | }); 145 | const result = formatter.format(source); 146 | expect(result).to.deep.equal(expected); 147 | 148 | }); 149 | 150 | it("should add semicolons when semicolons is true", () => { 151 | const source = "a\nb"; 152 | const expected = "a;\nb;\n"; 153 | const formatter = new Formatter({ 154 | style: { 155 | semicolons: true 156 | } 157 | }); 158 | const result = formatter.format(source); 159 | expect(result).to.deep.equal(expected); 160 | 161 | }); 162 | 163 | it("should add semicolons when semicolons omitted", () => { 164 | const source = "a\nb"; 165 | const expected = "a;\nb;\n"; 166 | const formatter = new Formatter({ 167 | style: { 168 | } 169 | }); 170 | const result = formatter.format(source); 171 | expect(result).to.deep.equal(expected); 172 | 173 | }); 174 | 175 | }); 176 | 177 | describe("maxEmptyLines", () => { 178 | it("should remove extra empty lines when maxEmptyLines is 1", () => { 179 | const source = "a;\n\n\nb;"; 180 | const expected = "a;\n\nb;\n"; 181 | const formatter = new Formatter({ 182 | style: { 183 | maxEmptyLines: 1 184 | } 185 | }); 186 | const result = formatter.format(source); 187 | expect(result).to.deep.equal(expected); 188 | }); 189 | 190 | it("should remove extra empty lines when the lines have whitespace and maxEmptyLines is 1", () => { 191 | const source = "if (f) {\na;\n\n \nb;\n}"; 192 | const expected = "if (f) {\n a;\n\n b;\n}\n"; 193 | const formatter = new Formatter({ 194 | style: { 195 | maxEmptyLines: 1 196 | } 197 | }); 198 | const result = formatter.format(source); 199 | expect(result).to.deep.equal(expected); 200 | }); 201 | 202 | }); 203 | 204 | }); 205 | 206 | describe("One-offs", () => { 207 | it("should not add a semicolon after last export", () => { 208 | const source = ` 209 | a(\`hello \${ 210 | world 211 | }\`); 212 | `.trim(); 213 | const expected = ` 214 | a(\`hello \${ 215 | world 216 | }\`); 217 | `.trim(); 218 | const formatter = new Formatter({ 219 | style: { 220 | maxEmptyLines: 2, 221 | emptyLastLine: false 222 | } 223 | }); 224 | const result = formatter.format(source); 225 | expect(result).to.deep.equal(expected); 226 | 227 | }); 228 | 229 | }); 230 | 231 | describe("fixtures", () => { 232 | const formatterFixturesPath = "./tests/fixtures/formatter"; 233 | fs.readdirSync(formatterFixturesPath).forEach(fileName => { 234 | 235 | const filePath = path.join(formatterFixturesPath, fileName); 236 | const contents = fs.readFileSync(filePath, "utf8").replace(/\r/g, ""); 237 | const [ options, source, expected ] = contents.split("\n---\n"); 238 | 239 | 240 | // if (!fileName.includes("template")) return; 241 | it(`Test in ${ fileName } should format correctly`, () => { 242 | const formatter = new Formatter({ 243 | style: JSON.parse(options) 244 | }); 245 | 246 | const result = formatter.format(source); 247 | expect(result.replace(/ /g, "\u00b7")).to.deep.equal(expected.replace(/ /g, "\u00b7")); 248 | }); 249 | }); 250 | }); 251 | 252 | }); 253 | -------------------------------------------------------------------------------- /tests/layout.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for layout 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { Layout } from "../src/layout.js"; 11 | import espree from "espree"; 12 | import chai from "chai"; 13 | import { SourceCode } from "../src/util/source-code.js"; 14 | 15 | const expect = chai.expect; 16 | 17 | //----------------------------------------------------------------------------- 18 | // Helpers 19 | //----------------------------------------------------------------------------- 20 | 21 | function parse(text) { 22 | return espree.parse(text, { range: true, tokens: true, comment: true, ecmaVersion: 2019, sourceType: "module" }); 23 | } 24 | 25 | const defaultOptions = { 26 | emptyLastLine: false 27 | }; 28 | 29 | function createLayout(sourceCode, options = {}) { 30 | return new Layout(sourceCode, { 31 | ...defaultOptions, 32 | ...options 33 | }); 34 | } 35 | 36 | function parseAndCreateLayout(text, options = {}) { 37 | const ast = parse(text); 38 | const sourceCode = new SourceCode(text, "foo.js", ast); 39 | return createLayout(sourceCode, options); 40 | } 41 | 42 | //----------------------------------------------------------------------------- 43 | // Tests 44 | //----------------------------------------------------------------------------- 45 | 46 | describe("Layout", () => { 47 | 48 | describe("Options", () => { 49 | describe("emptyLastLine", () => { 50 | 51 | it("should remove trailing whitespace on last line", () => { 52 | const text = "const a = {\n\n\n a: 5,\n b: 6,\n};\n "; 53 | const expected = "const a = {\n\n a: 5,\n b: 6\n};\n"; 54 | const layout = parseAndCreateLayout(text, { 55 | emptyLastLine: true 56 | }); 57 | 58 | expect(layout.toString()).to.equal(expected); 59 | 60 | }); 61 | 62 | it("should not make changes when the last line is empty", () => { 63 | const text = "const a = {\n\n\n a: 5,\n b: 6,\n};\n"; 64 | const expected = "const a = {\n\n a: 5,\n b: 6\n};\n"; 65 | const layout = parseAndCreateLayout(text, { 66 | emptyLastLine: true 67 | }); 68 | 69 | expect(layout.toString()).to.equal(expected); 70 | 71 | }); 72 | 73 | it("should add a line break if the last line is not empty", () => { 74 | const text = "const a = 5;"; 75 | const expected = "const a = 5;\n"; 76 | const layout = parseAndCreateLayout(text, { 77 | emptyLastLine: true 78 | }); 79 | 80 | expect(layout.toString()).to.equal(expected); 81 | 82 | }); 83 | 84 | }); 85 | describe("maxEmptyLines", () =>{ 86 | it("should remove empty line when the empty line has no whitespace", () => { 87 | const text = "const a = {\n\n\n a: 5,\n b: 6,\n};"; 88 | const expected = "const a = {\n\n a: 5,\n b: 6,\n};"; 89 | const layout = parseAndCreateLayout(text, { 90 | trailingCommas: true 91 | }); 92 | 93 | expect(layout.toString()).to.equal(expected); 94 | }); 95 | 96 | it("should remove empty line when the empty line has whitespace", () => { 97 | const text = "const a = {\n\n \n a: 5,\n b: 6,\n};"; 98 | const expected = "const a = {\n\n a: 5,\n b: 6,\n};"; 99 | const layout = parseAndCreateLayout(text, { 100 | trailingCommas: true, 101 | maxEmptyLines: 1 102 | }); 103 | 104 | expect(layout.toString()).to.equal(expected); 105 | }); 106 | 107 | it("should remove multiple empty lines when the empty lines have whitespace", () => { 108 | const text = "const a = {\n\n \n \n a: 5,\n b: 6,\n};"; 109 | const expected = "const a = {\n\n a: 5,\n b: 6,\n};"; 110 | const layout = parseAndCreateLayout(text, { 111 | trailingCommas: true 112 | }); 113 | 114 | expect(layout.toString()).to.equal(expected); 115 | }); 116 | 117 | it("should remove multiple empty lines when the empty lines have whitespace", () => { 118 | const text = "const a = {\n\n \n \n a: 5,\n b: 6,\n};"; 119 | const expected = "const a = {\n\n a: 5,\n b: 6,\n};"; 120 | const layout = parseAndCreateLayout(text, { 121 | trailingCommas: true 122 | }); 123 | 124 | expect(layout.toString()).to.equal(expected); 125 | }); 126 | }); 127 | }); 128 | 129 | 130 | describe("Indents", () => { 131 | 132 | it("should indent multiline block comments correctly when preceded by object literal", () => { 133 | const text = "const a = {\n a: b\n};\n\n/*a\n *b\n *c\n */"; 134 | const layout = parseAndCreateLayout(text, { 135 | trailingCommas: false 136 | }); 137 | 138 | expect(layout.toString()).to.equal(text); 139 | }); 140 | 141 | it("should remove whitespace tokens when their strings are empty", () => { 142 | const text = " `start`;"; 143 | const expected = "`start`;"; 144 | const ast = parse(text); 145 | const sourceCode = new SourceCode(text, "foo.js", ast); 146 | const layout = createLayout(sourceCode); 147 | 148 | const result = layout.findNext(token => token.type === "Whitespace", ast); 149 | expect(layout.toString()).to.equal(expected); 150 | expect(result).to.equal(undefined); 151 | }); 152 | 153 | }); 154 | 155 | describe("getIndentLevel()", () => { 156 | 157 | it("should return the correct indent level when the line has no indent", () => { 158 | const text = "a.b();"; 159 | const ast = parse(text); 160 | const sourceCode = new SourceCode(text, "foo.js", ast); 161 | const layout = createLayout(sourceCode); 162 | const level = layout.getIndentLevel(ast.body[0]); 163 | expect(level).to.equal(0); 164 | }); 165 | 166 | it("should return the correct indent level when the indent is one level", () => { 167 | const text = "{\n foo();\n}"; 168 | const ast = parse(text); 169 | const layout = createLayout({ ast, text }); 170 | const level = layout.getIndentLevel(ast.body[0].body[0]); 171 | expect(level).to.equal(1); 172 | }); 173 | 174 | it("should return the correct indent level when the indent is two levels", () => { 175 | const text = "{\n {\n foo();\n }\n}"; 176 | const ast = parse(text); 177 | const layout = createLayout({ ast, text }); 178 | const level = layout.getIndentLevel(ast.body[0].body[0].body[0]); 179 | expect(level).to.equal(2); 180 | }); 181 | 182 | }); 183 | 184 | describe("indentLevel()", () => { 185 | 186 | it("should indent one level when the code has no indent", () => { 187 | const text = "a.b();"; 188 | const ast = parse(text); 189 | const layout = createLayout({ ast, text }); 190 | 191 | expect(layout.indentLevel(ast.body[0], 1)).to.equal(true); 192 | const level = layout.getIndentLevel(ast.body[0]); 193 | expect(level).to.equal(1); 194 | }); 195 | 196 | it("should maintain the indent when passed the same indent level", () => { 197 | const text = "{\n foo();\n}"; 198 | const ast = parse(text); 199 | const layout = createLayout({ ast, text }); 200 | 201 | expect(layout.indentLevel(ast.body[0].body[0], 1)).to.equal(true); 202 | const level = layout.getIndentLevel(ast.body[0].body[0]); 203 | expect(level).to.equal(1); 204 | }); 205 | 206 | }); 207 | 208 | describe("getLength()", () => { 209 | 210 | it("should return the correct length when the line has no indent", () => { 211 | const text = "a.b();"; 212 | const ast = parse(text); 213 | const layout = createLayout({ ast, text }); 214 | const { firstToken, lastToken } = layout.boundaryTokens(ast.body[0].expression.callee); 215 | const length = layout.getLength(firstToken, lastToken); 216 | expect(text).to.equal(layout.toString()); 217 | expect(length).to.equal(3); 218 | }); 219 | 220 | }); 221 | 222 | 223 | describe("getLineLength()", () => { 224 | 225 | it("should return the correct line length when the line has no indent", () => { 226 | const text = "a.b();"; 227 | const ast = parse(text); 228 | const layout = createLayout({ ast, text }); 229 | const length = layout.getLineLength(ast.body[0]); 230 | expect(text).to.equal(layout.toString()); 231 | expect(length).to.equal(6); 232 | }); 233 | 234 | it("should return the correct line length when the line has an indent", () => { 235 | const text = "if (foo){\n a.b();\n}"; 236 | const ast = parse(text); 237 | const layout = createLayout({ ast, text }); 238 | const length = layout.getLineLength(ast.body[0].consequent); 239 | expect(length).to.equal(10); 240 | }); 241 | 242 | it("should return the correct line length when is inside an if condition", () => { 243 | const text = "if (foo){\nconst foo = [1, 2, 3, 4, 5];\n}"; 244 | const ast = parse(text); 245 | const layout = createLayout({ ast, text }); 246 | const length = layout.getLineLength(ast.body[0].consequent.body[0]); 247 | expect(length).to.equal(32); 248 | }); 249 | 250 | it("should return the correct line length when the line has a tab indent", () => { 251 | const text = "if (foo){\n\ta.b();\n}"; 252 | const ast = parse(text); 253 | const layout = createLayout({ ast, text }, { indent: "\t", tabWidth: 4 }); 254 | const length = layout.getLineLength(ast.body[0].consequent); 255 | expect(length).to.equal(10); 256 | }); 257 | 258 | }); 259 | 260 | describe("emptyLineAfter()", () => { 261 | 262 | it("should insert empty line when not found after node", () => { 263 | const text = "a;"; 264 | const expected = "a;\n\n"; 265 | const ast = parse(text); 266 | const layout = createLayout({ ast, text }); 267 | 268 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[0])); 269 | expect(layout.emptyLineAfter(semi)).to.be.true; 270 | expect(layout.toString()).to.equal(expected); 271 | }); 272 | 273 | it("should insert empty line when there's one line break after node", () => { 274 | const text = "a;\nb;"; 275 | const expected = "a;\n\nb;"; 276 | const ast = parse(text); 277 | const layout = createLayout({ ast, text }); 278 | 279 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[0])); 280 | expect(layout.emptyLineAfter(semi)).to.be.true; 281 | expect(layout.toString()).to.equal(expected); 282 | }); 283 | 284 | it("should insert empty line when there's one line break and whitespace after token", () => { 285 | const text = "a;\n b;"; 286 | const expected = "a;\n\nb;"; // note: spaces removed by indent behavior 287 | const ast = parse(text); 288 | const layout = createLayout({ ast, text }); 289 | 290 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[0])); 291 | expect(layout.emptyLineAfter(semi)).to.be.true; 292 | expect(layout.toString()).to.equal(expected); 293 | }); 294 | 295 | it("should insert empty line when no empty line after last token", () => { 296 | const text = "a;\nb;"; 297 | const expected = "a;\nb;\n\n"; 298 | const ast = parse(text); 299 | const layout = createLayout({ ast, text }); 300 | 301 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[1])); 302 | expect(layout.emptyLineAfter(semi)).to.be.true; 303 | expect(layout.toString()).to.equal(expected); 304 | }); 305 | 306 | it("should not insert empty line when empty line is after last token", () => { 307 | const text = "a;\nb;\n\n"; 308 | const expected = "a;\nb;\n\n"; 309 | const ast = parse(text); 310 | const layout = createLayout({ ast, text }); 311 | 312 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[1])); 313 | expect(layout.emptyLineAfter(semi)).to.be.false; 314 | expect(layout.toString()).to.equal(expected); 315 | }); 316 | 317 | it("should not insert empty line when there's an empty line after node", () => { 318 | const text = "a;\n\nb;"; 319 | const expected = "a;\n\nb;"; 320 | const ast = parse(text); 321 | const layout = createLayout({ ast, text }); 322 | 323 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[0])); 324 | expect(layout.emptyLineAfter(semi)).to.be.false; 325 | expect(layout.toString()).to.equal(expected); 326 | }); 327 | 328 | it("should not insert empty line when there's an empty line with whitespace after node", () => { 329 | const text = "a;\n \nb;"; 330 | const expected = "a;\n\nb;"; // note: extra spaces removed automatically 331 | const ast = parse(text); 332 | const layout = createLayout({ ast, text }); 333 | 334 | const semi = layout.findNext(token => token.value === ";", layout.lastToken(ast.body[0])); 335 | expect(layout.emptyLineAfter(semi)).to.be.false; 336 | expect(layout.toString()).to.equal(expected); 337 | }); 338 | 339 | }); 340 | 341 | describe("emptyLineBefore()", () => { 342 | 343 | it("should insert empty line when not found before first node", () => { 344 | const text = "a;"; 345 | const expected = "\na;"; 346 | const ast = parse(text); 347 | const layout = createLayout({ ast, text }); 348 | 349 | expect(layout.emptyLineBefore(ast.body[0])).to.be.true; 350 | expect(layout.toString()).to.equal(expected); 351 | }); 352 | 353 | it("should insert empty line when there's one line break before node", () => { 354 | const text = "a;\nb;"; 355 | const expected = "a;\n\nb;"; 356 | const ast = parse(text); 357 | const layout = createLayout({ ast, text }); 358 | 359 | expect(layout.emptyLineBefore(ast.body[1])).to.be.true; 360 | expect(layout.toString()).to.equal(expected); 361 | }); 362 | 363 | it("should insert empty line when there's no line break before node", () => { 364 | const text = "a;b;"; 365 | const expected = "a;\n\nb;"; 366 | const ast = parse(text); 367 | const layout = createLayout({ ast, text }); 368 | 369 | expect(layout.emptyLineBefore(ast.body[1])).to.be.true; 370 | expect(layout.toString()).to.equal(expected); 371 | }); 372 | 373 | it("should not insert empty line when empty line is before last node", () => { 374 | const text = "a;\n\nb;"; 375 | const expected = "a;\n\nb;"; 376 | const ast = parse(text); 377 | const layout = createLayout({ ast, text }); 378 | 379 | expect(layout.emptyLineBefore(ast.body[1])).to.be.false; 380 | expect(layout.toString()).to.equal(expected); 381 | }); 382 | 383 | it("should not insert empty line when there's an empty line before first node", () => { 384 | const text = "\n\na;"; 385 | const expected = "\n\na;"; 386 | const ast = parse(text); 387 | const layout = createLayout({ ast, text }); 388 | 389 | expect(layout.emptyLineBefore(ast.body[0])).to.be.false; 390 | expect(layout.toString()).to.equal(expected); 391 | }); 392 | 393 | }); 394 | 395 | describe("noEmptyLineAfter()", () => { 396 | 397 | it("should remove empty line when found after node", () => { 398 | const text = "a;\n\nb;"; 399 | const expected = "a;\nb;"; 400 | const ast = parse(text); 401 | const layout = createLayout({ ast, text }); 402 | 403 | layout.noEmptyLineAfter(ast.body[0]); 404 | expect(layout.toString()).to.equal(expected); 405 | }); 406 | 407 | it("should remove empty line when found after node with whitespace", () => { 408 | const text = "a;\n \nb;"; 409 | const expected = "a;\nb;"; 410 | const ast = parse(text); 411 | const layout = createLayout({ ast, text }); 412 | 413 | layout.noEmptyLineAfter(ast.body[0]); 414 | expect(layout.toString()).to.equal(expected); 415 | }); 416 | 417 | it("should remove empty line when found after token", () => { 418 | const text = "a;\n\nb;"; 419 | const expected = "a;\nb;"; 420 | const ast = parse(text); 421 | const layout = createLayout({ ast, text }); 422 | 423 | const token = layout.firstToken(ast); 424 | layout.noEmptyLineAfter(token); 425 | expect(layout.toString()).to.equal(expected); 426 | }); 427 | 428 | it("should remove empty line when found after token with whitespace", () => { 429 | const text = "a;\n \nb;"; 430 | const expected = "a;\nb;"; 431 | const ast = parse(text); 432 | const layout = createLayout({ ast, text }); 433 | 434 | const token = layout.firstToken(ast); 435 | layout.noEmptyLineAfter(token); 436 | expect(layout.toString()).to.equal(expected); 437 | }); 438 | 439 | it("should remove empty line when found after last node", () => { 440 | const text = "a;\nb;\n\n"; 441 | const expected = "a;\nb;\n"; 442 | const ast = parse(text); 443 | const layout = createLayout({ ast, text }); 444 | 445 | layout.noEmptyLineAfter(ast.body[1]); 446 | expect(layout.toString()).to.equal(expected); 447 | }); 448 | 449 | it("should not make changes when no empty line found after node", () => { 450 | const text = "a;\nb;"; 451 | const expected = "a;\nb;"; 452 | const ast = parse(text); 453 | const layout = createLayout({ ast, text }); 454 | 455 | layout.noEmptyLineAfter(ast.body[0]); 456 | expect(layout.toString()).to.equal(expected); 457 | }); 458 | 459 | it("should not make changes when no empty line found after token", () => { 460 | const text = "a;\nb;"; 461 | const expected = "a;\nb;"; 462 | const ast = parse(text); 463 | const layout = createLayout({ ast, text }); 464 | 465 | const token = layout.firstToken(ast); 466 | layout.noEmptyLineAfter(token); 467 | expect(layout.toString()).to.equal(expected); 468 | }); 469 | 470 | }); 471 | 472 | describe("noEmptyLineBefore()", () => { 473 | 474 | it("should remove empty line when found before node", () => { 475 | const text = "a;\n\nb;"; 476 | const expected = "a;\nb;"; 477 | const ast = parse(text); 478 | const layout = createLayout({ ast, text }); 479 | 480 | layout.noEmptyLineBefore(ast.body[1]); 481 | expect(layout.toString()).to.equal(expected); 482 | }); 483 | 484 | it("should remove empty line when found before with whitespace node", () => { 485 | const text = "a;\n \nb;"; 486 | const expected = "a;\nb;"; 487 | const ast = parse(text); 488 | const layout = createLayout({ ast, text }); 489 | 490 | layout.noEmptyLineBefore(ast.body[1]); 491 | expect(layout.toString()).to.equal(expected); 492 | }); 493 | 494 | it("should remove empty line when found before token", () => { 495 | const text = "a;\n\nb;"; 496 | const expected = "a;\nb;"; 497 | const ast = parse(text); 498 | const layout = createLayout({ ast, text }); 499 | 500 | const token = layout.firstToken(ast.body[1]); 501 | layout.noEmptyLineBefore(token); 502 | expect(layout.toString()).to.equal(expected); 503 | }); 504 | 505 | it("should remove empty line when found before token with whitespace", () => { 506 | const text = "a;\n \nb;"; 507 | const expected = "a;\nb;"; 508 | const ast = parse(text); 509 | const layout = createLayout({ ast, text }); 510 | 511 | const token = layout.firstToken(ast.body[1]); 512 | layout.noEmptyLineBefore(token); 513 | expect(layout.toString()).to.equal(expected); 514 | }); 515 | 516 | it("should remove empty line when found before first node", () => { 517 | const text = "\na;\nb;"; 518 | const expected = "a;\nb;"; 519 | const ast = parse(text); 520 | const layout = createLayout({ ast, text }); 521 | 522 | layout.noEmptyLineBefore(ast.body[0]); 523 | expect(layout.toString()).to.equal(expected); 524 | }); 525 | 526 | it("should remove empty line when found before first node with whitespace", () => { 527 | const text = " \na;\nb;"; 528 | const expected = "a;\nb;"; 529 | const ast = parse(text); 530 | const layout = createLayout({ ast, text }); 531 | 532 | layout.noEmptyLineBefore(ast.body[0]); 533 | expect(layout.toString()).to.equal(expected); 534 | }); 535 | 536 | 537 | it("should not make changes when no empty line found before node", () => { 538 | const text = "a;\nb;"; 539 | const expected = "a;\nb;"; 540 | const ast = parse(text); 541 | const layout = createLayout({ ast, text }); 542 | 543 | layout.noEmptyLineBefore(ast.body[1]); 544 | expect(layout.toString()).to.equal(expected); 545 | }); 546 | 547 | it("should not make changes when no empty line found before token", () => { 548 | const text = "a;\nb;"; 549 | const expected = "a;\nb;"; 550 | const ast = parse(text); 551 | const layout = createLayout({ ast, text }); 552 | 553 | const token = layout.firstToken(ast.body[1]); 554 | layout.noEmptyLineBefore(token); 555 | expect(layout.toString()).to.equal(expected); 556 | }); 557 | 558 | }); 559 | 560 | }); 561 | -------------------------------------------------------------------------------- /tests/util/token-list.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Tests for formatter 3 | * @author Nicholas C. Zakas 4 | */ 5 | 6 | //----------------------------------------------------------------------------- 7 | // Imports 8 | //----------------------------------------------------------------------------- 9 | 10 | import { TokenList } from "../../src/util/token-list.js"; 11 | import fs from "fs"; 12 | import path from "path"; 13 | import chai from "chai"; 14 | import espree from "espree"; 15 | 16 | const expect = chai.expect; 17 | 18 | //----------------------------------------------------------------------------- 19 | // Helpers 20 | //----------------------------------------------------------------------------- 21 | 22 | function parse(text) { 23 | return espree.parse(text, { range: true, tokens: true, comment: true, ecmaVersion: 2019, sourceType:"module" }); 24 | } 25 | 26 | //----------------------------------------------------------------------------- 27 | // Tests 28 | //----------------------------------------------------------------------------- 29 | 30 | describe("TokenList", () => { 31 | 32 | describe("add()", () => { 33 | 34 | it("should add two tokens in a row with next()/previous() links", () => { 35 | const tokenList = new TokenList(); 36 | const token1 = { 37 | type: "Foo", 38 | range: [0, 5] 39 | }; 40 | const token2 = { 41 | type: "Bar", 42 | range: [5, 10] 43 | }; 44 | 45 | tokenList.add(token1); 46 | tokenList.add(token2); 47 | 48 | expect(tokenList.first()).to.equal(token1); 49 | expect(tokenList.next(tokenList.first())).to.equal(token2); 50 | expect(tokenList.last()).to.equal(token2); 51 | expect(tokenList.previous(tokenList.last())).to.equal(token1); 52 | }); 53 | }); 54 | 55 | describe("delete()", () => { 56 | 57 | it("should delete token and remove from range maps when called", () => { 58 | const tokenList = new TokenList(); 59 | const token1 = { 60 | type: "Foo", 61 | range: [0, 5] 62 | }; 63 | const token2 = { 64 | type: "Bar", 65 | range: [5, 10] 66 | }; 67 | 68 | tokenList.add(token1); 69 | tokenList.add(token2); 70 | tokenList.delete(token1); 71 | 72 | expect(tokenList.getByRangeStart(0)).to.be.undefined; 73 | }); 74 | }); 75 | 76 | describe("findPreviousIndent()", () => { 77 | 78 | it("should find no previous indent when token has no indent", () => { 79 | const parts = [ 80 | { type: "Keyword", value: "const", range: [0, 5] }, 81 | { type: "Whitespace", value: " ", range: [5, 6] }, 82 | { type: "Identifier", value: "a", range: [6, 7] }, 83 | { type: "Whitespace", value: " ", range: [7, 8] }, 84 | { type: "Punctuator", value: "=", range: [8, 9] }, 85 | { type: "Whitespace", value: " ", range: [9, 10] }, 86 | { type: "Punctuator", value: "{", range: [10, 11] }, 87 | { type: "LineBreak", value: "\n", range: [11, 11] }, 88 | { type: "Whitespace", value: " ", range: [12, 16] }, 89 | { type: "Identifier", value: "a", range: [16, 17] }, 90 | { type: "Punctuator", value: ":", range: [17, 18] }, 91 | { type: "Whitespace", value: " ", range: [18, 19] }, 92 | { type: "Identifier", value: "b", range: [19, 20] }, 93 | { type: "LineBreak", value: "\n", range: [20, 20] }, 94 | { type: "Whitespace", value: "" }, 95 | { type: "Punctuator", value: "}", range: [21, 22] }, 96 | { type: "Punctuator", value: ";", range: [22, 23] }, 97 | { type: "LineBreak", value: "\n", range: [23, 23] }, 98 | { type: "LineBreak", value: "\n", range: [24, 24] }, 99 | { 100 | type: "BlockComment", 101 | value: "/*a\n *b\n *c\n */", 102 | range: [25, 40] 103 | } 104 | ]; 105 | 106 | const tokenList = new TokenList(parts); 107 | const maybeIndent = tokenList.findPreviousIndent(parts[parts.length - 1]); 108 | expect(maybeIndent).to.be.undefined; 109 | }); 110 | 111 | it("should find no previous indent when token has no indent", () => { 112 | const tokenList = new TokenList(); 113 | const token1 = { 114 | type: "Foo", 115 | range: [0, 5] 116 | }; 117 | const token2 = { 118 | type: "Bar", 119 | range: [5, 10] 120 | }; 121 | 122 | tokenList.add(token1); 123 | tokenList.add(token2); 124 | tokenList.delete(token1); 125 | 126 | const maybeIndent = tokenList.findPreviousIndent(token2); 127 | expect(maybeIndent).to.be.undefined; 128 | }); 129 | }); 130 | 131 | describe("fixtures", () => { 132 | const tokenListFixturesPath = "./tests/fixtures/token-list"; 133 | fs.readdirSync(tokenListFixturesPath).forEach(fileName => { 134 | 135 | const filePath = path.join(tokenListFixturesPath, fileName); 136 | const contents = fs.readFileSync(filePath, "utf8").replace(/\r/g, ""); 137 | const [ options, source, expected ] = contents.trim().split("\n---\n"); 138 | 139 | it(`Test in ${ fileName } should represent tokens correctly`, () => { 140 | const ast = parse(source); 141 | const tokenList = TokenList.fromAST(ast, source, JSON.parse(options)); 142 | expect([...tokenList]).to.deep.equal(JSON.parse(expected)); 143 | }); 144 | }); 145 | }); 146 | 147 | }); 148 | --------------------------------------------------------------------------------