├── .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 | 
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 |
--------------------------------------------------------------------------------