├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pr-gated.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── azure-pipelines.yml ├── package-lock.json ├── package.json ├── src ├── example.ts ├── index.ts ├── powerquery-formatter │ ├── format.ts │ ├── index.ts │ ├── passes │ │ ├── comment.ts │ │ ├── commonTypes.ts │ │ ├── index.ts │ │ ├── isMultiline │ │ │ ├── common.ts │ │ │ ├── isMultiline.ts │ │ │ ├── isMultilineFirstPass.ts │ │ │ ├── isMultilineSecondPass.ts │ │ │ └── linearLength.ts │ │ ├── preProcessParameter.ts │ │ ├── serializeParameter.ts │ │ └── utils │ │ │ └── linearLength.ts │ ├── serialize.ts │ ├── themes │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── register.ts │ │ ├── scopeNameHelpers.ts │ │ ├── scopes.ts │ │ ├── themes.ts │ │ ├── types.ts │ │ └── utils.ts │ └── trace.ts ├── scripts │ └── recursiveDirectoryFormat.ts └── test │ ├── comments.ts │ ├── common.ts │ ├── mochaConfig.json │ ├── section.ts │ ├── simple.ts │ └── smallPrograms.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | **/*.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | parserOptions: { project: "./tsconfig.json" }, 5 | plugins: ["@typescript-eslint", "prettier", "promise", "security"], 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:prettier/recommended", 10 | "plugin:security/recommended", 11 | ], 12 | rules: { 13 | "@typescript-eslint/await-thenable": "error", 14 | "@typescript-eslint/consistent-type-assertions": ["warn", { assertionStyle: "as" }], 15 | "@typescript-eslint/explicit-function-return-type": "error", 16 | "@typescript-eslint/no-floating-promises": "error", 17 | "@typescript-eslint/no-inferrable-types": "off", 18 | "@typescript-eslint/no-namespace": "error", 19 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 20 | "@typescript-eslint/prefer-namespace-keyword": "error", 21 | "@typescript-eslint/space-infix-ops": "error", 22 | "@typescript-eslint/switch-exhaustiveness-check": "error", 23 | "@typescript-eslint/typedef": [ 24 | "error", 25 | { 26 | arrayDestructuring: true, 27 | arrowParameter: true, 28 | memberVariableDeclaration: true, 29 | objectDestructuring: true, 30 | parameter: true, 31 | propertyDeclaration: true, 32 | variableDeclaration: true, 33 | }, 34 | ], 35 | "@typescript-eslint/unified-signatures": "error", 36 | "array-callback-return": "error", 37 | "arrow-body-style": ["error", "as-needed"], 38 | "constructor-super": "error", 39 | "max-len": [ 40 | "warn", 41 | { 42 | code: 120, 43 | ignorePattern: "^(?!.*/(/|\\*) .* .*).*$", 44 | }, 45 | ], 46 | "no-async-promise-executor": "error", 47 | "no-await-in-loop": "error", 48 | "no-class-assign": "error", 49 | "no-compare-neg-zero": "error", 50 | "no-cond-assign": "error", 51 | "no-constant-condition": "error", 52 | "no-dupe-class-members": "error", 53 | "no-dupe-else-if": "error", 54 | "no-dupe-keys": "error", 55 | "no-duplicate-imports": "error", 56 | "no-restricted-globals": "error", 57 | "no-eval": "error", 58 | "no-extra-boolean-cast": "error", 59 | "no-fallthrough": "error", 60 | "no-func-assign": "error", 61 | "no-global-assign": "error", 62 | "no-implicit-coercion": "error", 63 | "no-implicit-globals": "error", 64 | "no-implied-eval": "error", 65 | "no-invalid-this": "error", 66 | "no-irregular-whitespace": "error", 67 | "no-lone-blocks": "error", 68 | "no-lonely-if": "error", 69 | "no-loss-of-precision": "error", 70 | "no-nested-ternary": "error", 71 | "no-plusplus": "error", 72 | "no-self-assign": "error", 73 | "no-self-compare": "error", 74 | "no-sparse-arrays": "error", 75 | "no-this-before-super": "error", 76 | "no-unreachable": "error", 77 | "no-unsafe-optional-chaining": "error", 78 | "no-useless-backreference": "error", 79 | "no-useless-catch": "error", 80 | "no-useless-computed-key": "error", 81 | "no-useless-concat": "error", 82 | "no-useless-rename": "error", 83 | "no-useless-return": "error", 84 | "object-shorthand": ["error", "always"], 85 | "one-var": ["error", "never"], 86 | "padding-line-between-statements": [ 87 | "warn", 88 | { 89 | blankLine: "always", 90 | prev: "*", 91 | next: [ 92 | "class", 93 | "do", 94 | "for", 95 | "function", 96 | "if", 97 | "multiline-block-like", 98 | "multiline-const", 99 | "multiline-expression", 100 | "multiline-let", 101 | "multiline-var", 102 | "switch", 103 | "try", 104 | "while", 105 | ], 106 | }, 107 | { 108 | blankLine: "always", 109 | prev: [ 110 | "class", 111 | "do", 112 | "for", 113 | "function", 114 | "if", 115 | "multiline-block-like", 116 | "multiline-const", 117 | "multiline-expression", 118 | "multiline-let", 119 | "multiline-var", 120 | "switch", 121 | "try", 122 | "while", 123 | ], 124 | next: "*", 125 | }, 126 | { 127 | blankLine: "always", 128 | prev: "*", 129 | next: ["continue", "return"], 130 | }, 131 | ], 132 | "prefer-template": "error", 133 | "prettier/prettier": "error", 134 | "promise/prefer-await-to-then": "error", 135 | "require-atomic-updates": "error", 136 | "require-await": "warn", 137 | "security/detect-non-literal-fs-filename": "off", 138 | "security/detect-object-injection": "off", 139 | "spaced-comment": ["warn", "always"], 140 | "sort-imports": ["error", { allowSeparatedGroups: true, ignoreCase: true }], 141 | "valid-typeof": "error", 142 | }, 143 | }; 144 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: JordanBoltonMN 7 | 8 | --- 9 | 10 | **Expected behavior** 11 | A clear and concise description of what you expected to happen. 12 | 13 | **Actual behavior** 14 | A clear and concise description of the description of what the bug is. 15 | 16 | **To Reproduce** 17 | Please include the following: 18 | * (Required) The Power Query script that triggers the issue. 19 | * (Required) Any non-default settings used in the API call(s) which trigger the issue. 20 | * (Ideally) A minimal reproducible example. Can you reproduce the problem by calling a function in `src/example.ts`? 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Enhancement]" 5 | labels: enhancement 6 | assignees: JordanBoltonMN 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-gated.yml: -------------------------------------------------------------------------------- 1 | name: Gated pull request 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: [master] 6 | jobs: 7 | build-and-test: 8 | runs-on: windows-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v4.1.2 12 | - name: setup node 13 | uses: actions/setup-node@v4.0.2 14 | with: 15 | node-version: "18.17" 16 | - run: node -v 17 | - run: npm ci 18 | - run: npm audit 19 | - run: npm run build 20 | - run: npm run test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | lib/ 353 | tsconfig.tsbuildinfo 354 | test-results.xml 355 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "endOfLine": "auto", 4 | "printWidth": 120, 5 | "tabWidth": 4, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode", 5 | ], 6 | "unwantedRecommendations": [] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--debug", 14 | "--colors", 15 | "--timeout", 16 | "999999", 17 | "-r", 18 | "ts-node/register", 19 | "${workspaceFolder}/src/**/*.ts" 20 | ], 21 | "preLaunchTask": "build", 22 | "internalConsoleOptions": "openOnSessionStart" 23 | }, 24 | { 25 | "type": "node", 26 | "request": "launch", 27 | "name": "Launch Example", 28 | "program": "${workspaceFolder}\\src\\example.ts", 29 | "outFiles": ["${workspaceFolder}/**/*.js"], 30 | "preLaunchTask": "build" 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "files.exclude": { 4 | "src/**/*.js": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "typescript", 9 | "tsconfig": "tsconfig.json", 10 | "problemMatcher": [ 11 | "$tsc" 12 | ], 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/.*/ 2 | **/*.ts 3 | **/*.js.map 4 | .gitignore 5 | **/tsconfig.json 6 | **/tsconfig.base.json 7 | **/tsconfig.tsbuildinfo 8 | **/tslint.json 9 | **/package-lock.json 10 | contributing.md 11 | .travis.yml 12 | scripts/ 13 | packages/ 14 | client/** 15 | node_modules/ 16 | !client/dist/**/*.js 17 | server/** 18 | !server/dist/**/*.js 19 | **/*.zip 20 | **/*.vsix 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # powerquery-formatter 2 | 3 | [![Build Status](https://dev.azure.com/ms/powerquery-formatter/_apis/build/status/Microsoft.powerquery-formatter?branchName=master)](https://dev.azure.com/ms/powerquery-formatter/_build/latest?definitionId=343&branchName=master) 4 | 5 | This project contains a source code formatter for the Power Query / M language. 6 | 7 | ## Related projects 8 | 9 | - [powerquery-parser](https://github.com/microsoft/powerquery-parser): A lexer + parser for Power Query. Also contains features such as type validation. 10 | - [powerquery-language-services](https://github.com/microsoft/powerquery-language-services): A high level library that wraps the parser for external projects, such as the VSCode extension. Includes features such as Intellisense. 11 | - [vscode-powerquery](https://github.com/microsoft/vscode-powerquery): The VSCode extension for Power Query language support. 12 | - [vscode-powerquery-sdk](https://github.com/microsoft/vscode-powerquery-sdk): The VSCode extension for Power Query connector SDK. 13 | 14 | ## Build and test 15 | 16 | Build 17 | 18 | ```cmd 19 | npm install 20 | npm run build 21 | ``` 22 | 23 | Test 24 | 25 | ```cmd 26 | npm test 27 | ``` 28 | 29 | ## Contributing 30 | 31 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 32 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 33 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 34 | 35 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 36 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 37 | provided by the bot. You will only need to do this once across all repos using our CLA. 38 | 39 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 40 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 41 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js 2 | # Build a general Node.js project with npm. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | 6 | trigger: 7 | - master 8 | 9 | pr: 10 | - master 11 | 12 | pool: 13 | vmImage: 'windows-latest' 14 | 15 | steps: 16 | - task: NodeTool@0 17 | inputs: 18 | versionSpec: '16.x' 19 | displayName: 'Install Node.js' 20 | 21 | - task: Npm@1 22 | displayName: 'npm install' 23 | inputs: 24 | command: 'install' 25 | 26 | - task: Npm@1 27 | displayName: 'build' 28 | inputs: 29 | command: 'custom' 30 | customCommand: 'run build' 31 | 32 | - task: Npm@1 33 | displayName: 'npm test' 34 | inputs: 35 | command: 'custom' 36 | customCommand: 'test' 37 | 38 | - task: Npm@1 39 | displayName: 'pack' 40 | inputs: 41 | command: 'custom' 42 | customCommand: 'pack' 43 | 44 | - task: CopyFiles@2 45 | displayName: 'copy package to out directory' 46 | inputs: 47 | SourceFolder: '$(Build.SourcesDirectory)' 48 | Contents: '*.tgz' 49 | TargetFolder: '$(Build.SourcesDirectory)/out' 50 | OverWrite: true 51 | 52 | - task: PoliCheck@1 53 | inputs: 54 | inputType: 'Basic' 55 | targetType: 'F' 56 | targetArgument: 'src' 57 | result: 'PoliCheck.xml' 58 | 59 | - task: PublishTestResults@2 60 | condition: succeededOrFailed() 61 | inputs: 62 | testRunner: JUnit 63 | testResultsFiles: '**/test-results.xml' 64 | 65 | - task: PublishBuildArtifacts@1 66 | inputs: 67 | PathtoPublish: '$(System.DefaultWorkingDirectory)/lib' 68 | ArtifactName: lib 69 | displayName: 'publish lib' 70 | 71 | - task: PublishBuildArtifacts@1 72 | inputs: 73 | PathtoPublish: '$(System.DefaultWorkingDirectory)/out' 74 | ArtifactName: out 75 | displayName: 'publish out directory' 76 | 77 | - task: PublishBuildArtifacts@1 78 | inputs: 79 | PathtoPublish: '$(System.DefaultWorkingDirectory)/../_sdt/logs/PoliCheck' 80 | ArtifactName: PoliCheck 81 | displayName: 'publish policheck results' 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@microsoft/powerquery-formatter", 3 | "version": "0.3.16", 4 | "author": "Microsoft", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": ".\\node_modules\\.bin\\tsc", 8 | "watch": ".\\node_modules\\.bin\\tsc -b -watch", 9 | "test": "mocha --reporter mocha-multi-reporters --reporter-options configFile=src/test/mochaConfig.json -r ts-node/register src/test/**/*.ts", 10 | "script:recursiveDirectoryFormat": "npx ts-node src/scripts/recursiveDirectoryFormat.ts", 11 | "link:start": "npm link && npm uninstall @microsoft/powerquery-parser && git clean -xdf && npm install && npm link @microsoft/powerquery-parser", 12 | "link:stop": "npm unlink @microsoft/powerquery-parser && git clean -xdf && npm install && npm install @microsoft/powerquery-parser@latest --save-exact", 13 | "lint": "eslint src --ext ts", 14 | "prepublishOnly": "git clean -xdf && npm install-clean && npm run lint && npm run build && npm run test" 15 | }, 16 | "homepage": "https://github.com/microsoft/powerquery-formatter#readme", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/microsoft/powerquery-formatter.git" 20 | }, 21 | "issues": { 22 | "url": "https://github.com/microsoft/powerquery-formatter/issues" 23 | }, 24 | "description": "A source code formatter for the Power Query/M formula language.", 25 | "main": "lib/powerquery-formatter/index.js", 26 | "types": "lib/powerquery-formatter/index.d.ts", 27 | "engines": { 28 | "node": ">=16.13.1" 29 | }, 30 | "keywords": [ 31 | "power query", 32 | "power bi" 33 | ], 34 | "files": [ 35 | "lib/powerquery-formatter/**/*" 36 | ], 37 | "devDependencies": { 38 | "@types/chai": "^4.3.0", 39 | "@types/mocha": "^9.0.0", 40 | "@types/node": "^17.0.5", 41 | "@typescript-eslint/eslint-plugin": "5.8.1", 42 | "@typescript-eslint/parser": "5.8.1", 43 | "chai": "^4.3.4", 44 | "eslint": "8.5.0", 45 | "eslint-config-prettier": "8.3.0", 46 | "eslint-plugin-prettier": "4.0.0", 47 | "eslint-plugin-promise": "6.0.0", 48 | "eslint-plugin-security": "1.4.0", 49 | "mocha": "^11.1.0", 50 | "mocha-junit-reporter": "^2.0.2", 51 | "mocha-multi-reporters": "^1.5.1", 52 | "prettier": "^2.5.1", 53 | "ts-loader": "^9.2.6", 54 | "ts-node": "^10.4.0", 55 | "tslint": "^6.1.3", 56 | "typescript": "^4.5.4" 57 | }, 58 | "dependencies": { 59 | "@microsoft/powerquery-parser": "0.16.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/example.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | /* tslint:disable:no-console */ 5 | 6 | import * as PQP from "@microsoft/powerquery-parser"; 7 | 8 | import { FormatSettings, IndentationLiteral, NewlineLiteral, TriedFormat, tryFormat } from "."; 9 | 10 | const text: string = `1 as number`; 11 | 12 | const settings: FormatSettings = { 13 | ...PQP.DefaultSettings, 14 | indentationLiteral: IndentationLiteral.SpaceX4, 15 | maxWidth: 120, 16 | newlineLiteral: NewlineLiteral.Unix, 17 | }; 18 | 19 | tryFormat(settings, text) 20 | .then((triedFormat: TriedFormat) => { 21 | if (PQP.ResultUtils.isOk(triedFormat)) { 22 | console.log("Your input was formatted as the following:"); 23 | console.log(triedFormat.value); 24 | } else { 25 | console.log("An error occured during the format. Please review the error."); 26 | console.log(JSON.stringify(triedFormat.error, undefined, 4)); 27 | } 28 | }) 29 | .catch(() => console.log("An uncaught error was thrown")); 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * from "./powerquery-formatter"; 5 | -------------------------------------------------------------------------------- /src/powerquery-formatter/format.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 6 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 7 | 8 | import { 9 | CommentCollection, 10 | CommentCollectionMap, 11 | CommentResult, 12 | SerializeParameterMap, 13 | tryPreProcessParameter, 14 | tryTraverseComment, 15 | tryTraverseSerializeParameter, 16 | } from "./passes"; 17 | import { 18 | IndentationLiteral, 19 | NewlineLiteral, 20 | SerializePassthroughMaps, 21 | SerializeSettings, 22 | TriedSerialize, 23 | trySerialize, 24 | } from "./serialize"; 25 | import { FormatTraceConstant } from "./trace"; 26 | import { SyncThemeRegistry } from "./themes"; 27 | 28 | export type TriedFormat = PQP.Result; 29 | 30 | export type TFormatError = 31 | | PQP.CommonError.CommonError 32 | | PQP.Lexer.LexError.TLexError 33 | | PQP.Parser.ParseError.TParseError; 34 | 35 | export interface FormatSettings extends PQP.Settings { 36 | readonly indentationLiteral: IndentationLiteral; 37 | readonly newlineLiteral: NewlineLiteral; 38 | readonly maxWidth: number; 39 | } 40 | 41 | export const DefaultSettings: FormatSettings = { 42 | ...PQP.DefaultSettings, 43 | indentationLiteral: IndentationLiteral.SpaceX4, 44 | maxWidth: 120, 45 | newlineLiteral: NewlineLiteral.Windows, 46 | }; 47 | 48 | export async function tryFormat(formatSettings: FormatSettings, text: string): Promise { 49 | const trace: Trace = formatSettings.traceManager.entry( 50 | FormatTraceConstant.Format, 51 | tryFormat.name, 52 | formatSettings.initialCorrelationId, 53 | ); 54 | 55 | const triedLexParse: PQP.Task.TriedLexParseTask = await PQP.TaskUtils.tryLexParse( 56 | { 57 | ...formatSettings, 58 | initialCorrelationId: trace.id, 59 | }, 60 | text, 61 | ); 62 | 63 | if (PQP.TaskUtils.isError(triedLexParse)) { 64 | return PQP.ResultUtils.error(triedLexParse.error); 65 | } 66 | 67 | const ast: Ast.TNode = triedLexParse.ast; 68 | const comments: ReadonlyArray = triedLexParse.lexerSnapshot.comments; 69 | const nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection = triedLexParse.nodeIdMapCollection; 70 | 71 | const locale: string = formatSettings.locale; 72 | const traceManager: TraceManager = formatSettings.traceManager; 73 | const cancellationToken: PQP.ICancellationToken | undefined = formatSettings.cancellationToken; 74 | 75 | let commentCollectionMap: CommentCollectionMap = new Map(); 76 | 77 | let eofCommentCollection: CommentCollection = { 78 | prefixedComments: [], 79 | prefixedCommentsContainsNewline: false, 80 | }; 81 | 82 | const containerIdHavingComments: Set = new Set(); 83 | 84 | if (comments.length) { 85 | const triedCommentPass: PQP.Traverse.TriedTraverse = await tryTraverseComment( 86 | ast, 87 | nodeIdMapCollection, 88 | comments, 89 | locale, 90 | traceManager, 91 | trace.id, 92 | cancellationToken, 93 | ); 94 | 95 | if (PQP.ResultUtils.isError(triedCommentPass)) { 96 | return triedCommentPass; 97 | } 98 | 99 | commentCollectionMap = triedCommentPass.value.commentCollectionMap; 100 | eofCommentCollection = triedCommentPass.value.eofCommentCollection; 101 | 102 | const containerIdHavingCommentsChildCount: Map = 103 | triedCommentPass.value.containerIdHavingCommentsChildCount; 104 | 105 | const parentContainerIdOfNodeId: Map = triedCommentPass.value.parentContainerIdOfNodeId; 106 | 107 | for (const [nodeId, commentCollection] of commentCollectionMap) { 108 | const isLastCommentContainingNewLine: boolean = commentCollection.prefixedComments.length 109 | ? commentCollection.prefixedComments[commentCollection.prefixedComments.length - 1].containsNewline 110 | : false; 111 | 112 | const parentContainerId: number = parentContainerIdOfNodeId.get(nodeId) ?? 0; 113 | 114 | const currentChildIds: ReadonlyArray = parentContainerId 115 | ? nodeIdMapCollection.childIdsById.get(parentContainerId) ?? [] 116 | : []; 117 | 118 | // if the last comment contained a new line, we definitely gonna append a new line after it 119 | // therefore, if the current literal token were first child of the closet container, 120 | // we could render the container in in-line mode 121 | if ( 122 | isLastCommentContainingNewLine && 123 | parentContainerId && 124 | currentChildIds.length && 125 | currentChildIds[0] === nodeId 126 | ) { 127 | // we found one first literal token of matched comments right beneath the container, 128 | // thus we need to decrease parent's comment child count by one of that container 129 | let currentChildCount: number = containerIdHavingCommentsChildCount.get(parentContainerId) ?? 1; 130 | currentChildCount -= 1; 131 | containerIdHavingCommentsChildCount.set(parentContainerId, currentChildCount); 132 | } 133 | } 134 | 135 | // therefore, we only need to populate containerIdHavingComments of child comment greater than zero 136 | for (const [containerId, childCommentCounts] of containerIdHavingCommentsChildCount) { 137 | if (childCommentCounts > 0) { 138 | containerIdHavingComments.add(containerId); 139 | } 140 | } 141 | } 142 | 143 | // move its static as a singleton instant for now 144 | const newRegistry: SyncThemeRegistry = SyncThemeRegistry.defaultInstance; 145 | 146 | const triedSerializeParameter: PQP.Traverse.TriedTraverse = 147 | await tryTraverseSerializeParameter( 148 | ast, 149 | nodeIdMapCollection, 150 | commentCollectionMap, 151 | newRegistry.scopeMetaProvider, 152 | locale, 153 | formatSettings.traceManager, 154 | trace.id, 155 | cancellationToken, 156 | ); 157 | 158 | if (PQP.ResultUtils.isError(triedSerializeParameter)) { 159 | return triedSerializeParameter; 160 | } 161 | 162 | const serializeParameterMap: SerializeParameterMap = triedSerializeParameter.value; 163 | 164 | const triedPreProcessedSerializeParameter: PQP.Traverse.TriedTraverse = 165 | await tryPreProcessParameter( 166 | ast, 167 | nodeIdMapCollection, 168 | commentCollectionMap, 169 | formatSettings.traceManager, 170 | locale, 171 | trace.id, 172 | cancellationToken, 173 | serializeParameterMap, 174 | ); 175 | 176 | if (PQP.ResultUtils.isError(triedPreProcessedSerializeParameter)) { 177 | return triedPreProcessedSerializeParameter; 178 | } 179 | 180 | const passthroughMaps: SerializePassthroughMaps = { 181 | commentCollectionMap, 182 | eofCommentCollection, 183 | containerIdHavingComments, 184 | serializeParameterMap, 185 | }; 186 | 187 | const serializeRequest: SerializeSettings = { 188 | locale, 189 | ast, 190 | text, 191 | nodeIdMapCollection, 192 | passthroughMaps, 193 | indentationLiteral: formatSettings.indentationLiteral, 194 | traceManager: formatSettings.traceManager, 195 | newlineLiteral: formatSettings.newlineLiteral, 196 | cancellationToken: undefined, 197 | initialCorrelationId: trace.id, 198 | // everytime `trySerialize` using this maxWidth, 199 | // it would assume there would be two extra spaces where it could append whitespaces 200 | maxWidth: formatSettings.maxWidth - 2, 201 | }; 202 | 203 | const triedSerialize: TriedSerialize = await trySerialize(serializeRequest); 204 | trace.exit(); 205 | 206 | return triedSerialize; 207 | } 208 | -------------------------------------------------------------------------------- /src/powerquery-formatter/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * from "./format"; 5 | export * from "./serialize"; 6 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/comment.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 6 | import { TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 7 | 8 | import { CommentCollection, CommentCollectionMap, CommentResult, CommentState } from "./commonTypes"; 9 | import { ContainerSet } from "../themes"; 10 | 11 | const containerNodeKindSet: ReadonlySet = ContainerSet; 12 | 13 | export async function tryTraverseComment( 14 | root: Ast.TNode, 15 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 16 | comments: ReadonlyArray, 17 | locale: string, 18 | traceManager: TraceManager, 19 | correlationId: number | undefined, 20 | cancellationToken: PQP.ICancellationToken | undefined, 21 | ): Promise> { 22 | const state: CommentState = { 23 | locale, 24 | traceManager, 25 | cancellationToken, 26 | initialCorrelationId: correlationId, 27 | result: { 28 | commentCollectionMap: new Map(), 29 | containerIdHavingCommentsChildCount: new Map(), 30 | parentContainerIdOfNodeId: new Map(), 31 | eofCommentCollection: { 32 | prefixedComments: [], 33 | prefixedCommentsContainsNewline: false, 34 | }, 35 | }, 36 | nodeIdMapCollection, 37 | comments, 38 | leafIdsOfItsContainerFound: new Set(), 39 | commentsIndex: 0, 40 | currentComment: comments[0], 41 | }; 42 | 43 | const triedCommentPass: PQP.Traverse.TriedTraverse = await PQP.Traverse.tryTraverseAst< 44 | CommentState, 45 | CommentResult 46 | >( 47 | state, 48 | nodeIdMapCollection, 49 | root, 50 | PQP.Traverse.VisitNodeStrategy.DepthFirst, 51 | visitNode, 52 | PQP.Traverse.assertGetAllAstChildren, 53 | earlyExit, 54 | ); 55 | 56 | // check whether we got any comment prefixed to the EOF 57 | if (!PQP.ResultUtils.isError(triedCommentPass) && state.commentsIndex < state.comments.length) { 58 | const result: CommentResult = triedCommentPass.value; 59 | let prefixedCommentsContainsNewline: boolean = false; 60 | result.eofCommentCollection.prefixedComments.length = 0; 61 | 62 | state.comments 63 | .slice(state.commentsIndex, state.comments.length) 64 | .forEach((comment: PQP.Language.Comment.TComment) => { 65 | result.eofCommentCollection.prefixedComments.push(comment); 66 | prefixedCommentsContainsNewline = prefixedCommentsContainsNewline || comment.containsNewline; 67 | }); 68 | 69 | result.eofCommentCollection.prefixedCommentsContainsNewline = prefixedCommentsContainsNewline; 70 | } 71 | 72 | return triedCommentPass; 73 | } 74 | 75 | // eslint-disable-next-line require-await 76 | async function earlyExit(state: CommentState, node: Ast.TNode): Promise { 77 | const currentComment: PQP.Language.Comment.TComment | undefined = state.currentComment; 78 | 79 | if (currentComment === undefined) { 80 | return true; 81 | } else if (node.tokenRange.positionEnd.codeUnit < currentComment.positionStart.codeUnit) { 82 | return true; 83 | } else { 84 | return false; 85 | } 86 | } 87 | 88 | // eslint-disable-next-line require-await 89 | async function visitNode(state: CommentState, node: Ast.TNode): Promise { 90 | if (!node.isLeaf) { 91 | return; 92 | } 93 | 94 | const nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection = state.nodeIdMapCollection; 95 | let currentComment: PQP.Language.Comment.TComment | undefined = state.currentComment; 96 | const leafIdsOfItsContainerFound: Set = state.leafIdsOfItsContainerFound; 97 | const commentMap: CommentCollectionMap = state.result.commentCollectionMap; 98 | const containerIdHavingCommentsChildCount: Map = state.result.containerIdHavingCommentsChildCount; 99 | const parentContainerIdOfNodeId: Map = state.result.parentContainerIdOfNodeId; 100 | const nodeId: number = node.id; 101 | 102 | while (currentComment && currentComment.positionStart.codeUnit < node.tokenRange.positionStart.codeUnit) { 103 | const commentCollection: CommentCollection | undefined = commentMap.get(nodeId); 104 | 105 | // It's the first comment for the TNode 106 | if (commentCollection === undefined) { 107 | const commentCollection: CommentCollection = { 108 | prefixedComments: [currentComment], 109 | prefixedCommentsContainsNewline: currentComment.containsNewline, 110 | }; 111 | 112 | commentMap.set(nodeId, commentCollection); 113 | } 114 | // At least one comment already attached to the TNode 115 | else { 116 | commentCollection.prefixedComments.push(currentComment); 117 | 118 | if (currentComment.containsNewline) { 119 | commentCollection.prefixedCommentsContainsNewline = true; 120 | } 121 | } 122 | 123 | // alright we got a leaf node having comments 124 | if (!leafIdsOfItsContainerFound.has(nodeId)) { 125 | // trace up to find it the closest ancestry and mark it at containIdsHavingComments 126 | let parentId: number | undefined = nodeIdMapCollection.parentIdById.get(nodeId); 127 | 128 | while (parentId) { 129 | const parent: PQP.Language.Ast.TNode | undefined = nodeIdMapCollection.astNodeById.get(parentId); 130 | 131 | if (parent?.kind && containerNodeKindSet.has(parent?.kind)) { 132 | let currentChildCount: number = containerIdHavingCommentsChildCount.get(parentId) ?? 0; 133 | currentChildCount += 1; 134 | containerIdHavingCommentsChildCount.set(parentId, currentChildCount); 135 | parentContainerIdOfNodeId.set(nodeId, parentId); 136 | leafIdsOfItsContainerFound.add(nodeId); 137 | break; 138 | } 139 | 140 | parentId = nodeIdMapCollection.parentIdById.get(parentId); 141 | } 142 | } 143 | 144 | state.commentsIndex += 1; 145 | currentComment = state.comments[state.commentsIndex]; 146 | } 147 | 148 | state.currentComment = currentComment; 149 | } 150 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/commonTypes.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | // Holds all types, enums, and interfaces used by tryTraverseSerializeParameter. 5 | 6 | import * as PQP from "@microsoft/powerquery-parser"; 7 | 8 | export type CommentCollectionMap = Map; 9 | export type IsMultilineMap = Map; 10 | export type LinearLengthMap = Map; 11 | 12 | export interface CommentCollection { 13 | readonly prefixedComments: PQP.Language.Comment.TComment[]; 14 | prefixedCommentsContainsNewline: boolean; 15 | } 16 | 17 | export interface CommentResult { 18 | readonly commentCollectionMap: CommentCollectionMap; 19 | readonly eofCommentCollection: CommentCollection; 20 | } 21 | 22 | export interface CommentState extends PQP.Traverse.ITraversalState { 23 | readonly comments: ReadonlyArray; 24 | commentsIndex: number; 25 | currentComment: PQP.Language.Comment.TComment | undefined; 26 | } 27 | 28 | export interface IsMultilineFirstPassState extends PQP.Traverse.ITraversalState { 29 | readonly commentCollectionMap: CommentCollectionMap; 30 | readonly linearLengthMap: LinearLengthMap; 31 | readonly nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection; 32 | } 33 | 34 | export interface IsMultilineSecondPassState extends PQP.Traverse.ITraversalState { 35 | readonly nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection; 36 | } 37 | 38 | export interface LinearLengthState extends PQP.Traverse.ITraversalState { 39 | readonly linearLengthMap: LinearLengthMap; 40 | readonly nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection; 41 | } 42 | 43 | export enum SerializeWriteKind { 44 | Any = "Any", 45 | DoubleNewline = "DoubleNewline", 46 | Indented = "Indented", 47 | PaddedLeft = "PaddedLeft", 48 | PaddedRight = "PaddedRight", 49 | } 50 | 51 | export interface SerializeCommentParameter { 52 | readonly literal: string; 53 | readonly writeKind: SerializeWriteKind; 54 | } 55 | 56 | export interface CommentResult { 57 | readonly commentCollectionMap: CommentCollectionMap; 58 | readonly containerIdHavingCommentsChildCount: Map; 59 | readonly parentContainerIdOfNodeId: Map; 60 | readonly eofCommentCollection: CommentCollection; 61 | } 62 | 63 | export interface CommentState extends PQP.Traverse.ITraversalState { 64 | readonly nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection; 65 | readonly comments: ReadonlyArray; 66 | /** 67 | * The leaf ids whose closest container was found already 68 | */ 69 | readonly leafIdsOfItsContainerFound: Set; 70 | commentsIndex: number; 71 | currentComment: PQP.Language.Comment.TComment | undefined; 72 | } 73 | 74 | export type Offset = "L" | "R"; 75 | 76 | export type SerializeParameter = Partial<{ 77 | /** 78 | * container, a boolean field defines whether the current ast node is a container of blocks 79 | * - a container will persis the indent level unchanged before entering and after leaving it 80 | */ 81 | container: boolean; 82 | /** 83 | * indentContainerOnEnter, a container field: 84 | * indent the container when entering the container in block mode 85 | */ 86 | indentContainerOnEnter: boolean; 87 | /** 88 | * dedentContainerConditionReg, a container field: 89 | * a regex that will decrease the current indent level by one if the present formatted line matches it 90 | */ 91 | dedentContainerConditionReg: RegExp; 92 | /** 93 | * skipPostContainerNewLine, a container field: 94 | * once set truthy, it would skip putting a new line when formatter leaving the container 95 | */ 96 | skipPostContainerNewLine: boolean; 97 | /** 98 | * ignoreInline, a container field: 99 | * once set truthy, it will force the present container being formatted in the block mode when entering it, 100 | * and ignoreInline is of lower-priority, when a nested container could be fit with the max-width, 101 | * that nested container could still be formatted in the in-line mode. 102 | */ 103 | ignoreInline: boolean; 104 | /** 105 | * forceBlockMode, a container field: 106 | * once set truthy, it will force the present container being formatted in the block mode when entering it, 107 | * even the container could be fit with the max-width 108 | */ 109 | forceBlockMode: boolean; 110 | /** 111 | * inheritParentMode, a container field: 112 | * once set truthy, it will force the present container inheriting formatting mode from its closest parent container 113 | */ 114 | inheritParentMode: boolean; 115 | /** 116 | * blockOpener, a block or container field: 117 | * define an opener anchor relative to the current token, which could be either 'L' or 'R', which starts a block 118 | * 'L' means the opener is on the left-hand side of the token, and 'R' for the right-hand side 119 | */ 120 | blockOpener: Offset; 121 | /** 122 | * blockOpenerActivatedMatcher, a block field: 123 | * a regex that would only activate the block opener when it matches the text divided by the current token 124 | * when the blockOpener was set 'L', the regex should try to match the text on left-hand side of the token 125 | * when the blockOpener was set 'R', the regex should try to match the text on right-hand side of the token 126 | */ 127 | blockOpenerActivatedMatcher: RegExp; 128 | /** 129 | * blockOpener, a block or container field: 130 | * define a closer anchor relative to the current token, which could be either 'L' or 'R', which ends the block 131 | * 'L' means the closer is on the left-hand side of the token, and 'R' for the right-hand side 132 | */ 133 | blockCloser: Offset; 134 | /** 135 | * noWhiteSpaceBetweenWhenNoContentBetweenOpenerAndCloser, a block closer field: 136 | * wipe out any white spaces only if there were no other tokens between current closer anchor and its opener 137 | */ 138 | noWhiteSpaceBetweenWhenNoContentBetweenOpenerAndCloser: boolean; 139 | /** 140 | * contentDivider, a block or container field: 141 | * define a divide anchor relative to the current token, which could be either 'L' or 'R', which would divide tokens 142 | * 'L' means the closer is on the left-hand side of the token, and 'R' for the right-hand side 143 | * In a block mode container, the divider would turn into a new line 144 | * In an in-line mode container, the divider should be either a space if it fits or empty instead 145 | */ 146 | contentDivider: Offset; 147 | /** 148 | * leftPadding, a token field: 149 | * suggest there should be a padding white space on the left-hand side of the token 150 | */ 151 | leftPadding: boolean; 152 | /** 153 | * rightPadding, a token field: 154 | * suggest there should be a padding white space on the right-hand side of the token 155 | */ 156 | rightPadding: boolean; 157 | /** 158 | * lineBreak, a token field: 159 | * suggest append a new line on the right-hand side of the token 160 | */ 161 | lineBreak: Offset; 162 | /** 163 | * doubleLineBreak, a token field: 164 | * suggest append two new lines on the right-hand side of the token 165 | */ 166 | doubleLineBreak: Offset; 167 | /** 168 | * noWhitespaceAppended, a token field: 169 | * avoid appending any white spaces after the current token before another no-whitespace literal token appended 170 | */ 171 | noWhitespaceAppended: boolean; 172 | /** 173 | * clearTailingWhitespaceBeforeAppending, a token field: 174 | * clean up any white spaces behind the previously appended non-whitespace literal token 175 | * and then append the current token 176 | */ 177 | clearTailingWhitespaceBeforeAppending: boolean; 178 | /** 179 | * clearTailingWhitespaceCarriageReturnBeforeAppending, a token field: 180 | * clean up any white spaces including crlf and lf behind the previously appended non-whitespace literal token 181 | * and then append the current token 182 | */ 183 | clearTailingWhitespaceCarriageReturnBeforeAppending: boolean; 184 | }>; 185 | 186 | export interface SerializeParameterMap { 187 | readonly parametersMap: Map; 188 | } 189 | 190 | export interface SerializeParameterState extends PQP.Traverse.ITraversalState { 191 | readonly commentCollectionMap: CommentCollectionMap; 192 | readonly nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection; 193 | } 194 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export { tryPreProcessParameter } from "./preProcessParameter"; 5 | export { tryTraverseSerializeParameter } from "./serializeParameter"; 6 | export { tryTraverseComment } from "./comment"; 7 | export * from "./commonTypes"; 8 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/isMultiline/common.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 6 | 7 | import { IsMultilineMap } from "../commonTypes"; 8 | 9 | export function expectGetIsMultiline(isMultilineMap: IsMultilineMap, node: Ast.TNode): boolean { 10 | return PQP.MapUtils.assertGet(isMultilineMap, node.id, "missing expected nodeId", { nodeId: node.id }); 11 | } 12 | 13 | export function setIsMultiline(isMultilineMap: IsMultilineMap, node: Ast.TNode, isMultiline: boolean): void { 14 | isMultilineMap.set(node.id, isMultiline); 15 | } 16 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/isMultiline/isMultiline.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 6 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 7 | 8 | import { CommentCollectionMap, IsMultilineMap } from "../commonTypes"; 9 | import { FormatTraceConstant } from "../../trace"; 10 | import { tryTraverseIsMultilineFirstPass } from "./isMultilineFirstPass"; 11 | import { tryTraverseIsMultilineSecondPass } from "./isMultilineSecondPass"; 12 | 13 | // runs a DFS pass followed by a BFS pass. 14 | export async function tryTraverseIsMultiline( 15 | ast: Ast.TNode, 16 | commentCollectionMap: CommentCollectionMap, 17 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 18 | locale: string, 19 | traceManager: TraceManager, 20 | correlationId: number | undefined, 21 | cancellationToken: PQP.ICancellationToken | undefined, 22 | ): Promise> { 23 | const trace: Trace = traceManager.entry( 24 | FormatTraceConstant.IsMultiline, 25 | tryTraverseIsMultiline.name, 26 | correlationId, 27 | ); 28 | 29 | const triedFirstPass: PQP.Traverse.TriedTraverse = await tryTraverseIsMultilineFirstPass( 30 | ast, 31 | commentCollectionMap, 32 | nodeIdMapCollection, 33 | locale, 34 | traceManager, 35 | trace.id, 36 | cancellationToken, 37 | ); 38 | 39 | if (PQP.ResultUtils.isError(triedFirstPass)) { 40 | return triedFirstPass; 41 | } 42 | 43 | const isMultilineMap: IsMultilineMap = triedFirstPass.value; 44 | 45 | const result: PQP.Traverse.TriedTraverse = await tryTraverseIsMultilineSecondPass( 46 | ast, 47 | isMultilineMap, 48 | nodeIdMapCollection, 49 | locale, 50 | traceManager, 51 | trace.id, 52 | cancellationToken, 53 | ); 54 | 55 | trace.exit(); 56 | 57 | return result; 58 | } 59 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/isMultiline/isMultilineFirstPass.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Ast, AstUtils } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 6 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 7 | import { FormatTraceConstant } from "../../trace"; 8 | 9 | import { 10 | CommentCollection, 11 | CommentCollectionMap, 12 | IsMultilineFirstPassState, 13 | IsMultilineMap, 14 | LinearLengthMap, 15 | } from "../commonTypes"; 16 | import { expectGetIsMultiline, setIsMultiline } from "./common"; 17 | import { getLinearLength } from "./linearLength"; 18 | 19 | export function tryTraverseIsMultilineFirstPass( 20 | ast: Ast.TNode, 21 | commentCollectionMap: CommentCollectionMap, 22 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 23 | locale: string, 24 | traceManager: TraceManager, 25 | correlationId: number | undefined, 26 | cancellationToken: PQP.ICancellationToken | undefined, 27 | ): Promise> { 28 | const state: IsMultilineFirstPassState = { 29 | locale, 30 | traceManager, 31 | cancellationToken, 32 | initialCorrelationId: correlationId, 33 | commentCollectionMap, 34 | linearLengthMap: new Map(), 35 | nodeIdMapCollection, 36 | result: new Map(), 37 | }; 38 | 39 | return PQP.Traverse.tryTraverseAst( 40 | state, 41 | nodeIdMapCollection, 42 | ast, 43 | PQP.Traverse.VisitNodeStrategy.DepthFirst, 44 | visitNode, 45 | PQP.Traverse.assertGetAllAstChildren, 46 | undefined, 47 | ); 48 | } 49 | 50 | const InvokeExpressionIdentifierLinearLengthExclusions: ReadonlyArray = [ 51 | "#datetime", 52 | "#datetimezone", 53 | "#duration", 54 | "#time", 55 | ]; 56 | 57 | const TBinOpExpressionLinearLengthThreshold: number = 40; 58 | const InvokeExpressionLinearLengthThreshold: number = 40; 59 | 60 | async function visitNode( 61 | state: IsMultilineFirstPassState, 62 | node: Ast.TNode, 63 | correlationId: number | undefined, 64 | ): Promise { 65 | const trace: Trace = state.traceManager.entry( 66 | FormatTraceConstant.IsMultilinePhase1, 67 | visitNode.name, 68 | correlationId, 69 | { 70 | nodeId: node.id, 71 | nodeKind: node.kind, 72 | }, 73 | ); 74 | 75 | const isMultilineMap: IsMultilineMap = state.result; 76 | let isMultiline: boolean = false; 77 | 78 | switch (node.kind) { 79 | // TPairedConstant 80 | case Ast.NodeKind.AsNullablePrimitiveType: 81 | case Ast.NodeKind.AsType: 82 | case Ast.NodeKind.CatchExpression: 83 | case Ast.NodeKind.EachExpression: 84 | case Ast.NodeKind.ErrorRaisingExpression: 85 | case Ast.NodeKind.IsNullablePrimitiveType: 86 | case Ast.NodeKind.NullablePrimitiveType: 87 | case Ast.NodeKind.NullableType: 88 | case Ast.NodeKind.OtherwiseExpression: 89 | case Ast.NodeKind.TypePrimaryType: 90 | isMultiline = isAnyMultiline(isMultilineMap, node.constant, node.paired); 91 | break; 92 | 93 | // TBinOpExpression 94 | case Ast.NodeKind.ArithmeticExpression: 95 | case Ast.NodeKind.AsExpression: 96 | case Ast.NodeKind.EqualityExpression: 97 | case Ast.NodeKind.IsExpression: 98 | case Ast.NodeKind.LogicalExpression: 99 | case Ast.NodeKind.NullCoalescingExpression: 100 | case Ast.NodeKind.RelationalExpression: 101 | isMultiline = await visitBinOpExpression(state, node, isMultilineMap, trace.id); 102 | break; 103 | 104 | // TKeyValuePair 105 | case Ast.NodeKind.GeneralizedIdentifierPairedAnyLiteral: 106 | case Ast.NodeKind.GeneralizedIdentifierPairedExpression: 107 | case Ast.NodeKind.IdentifierPairedExpression: 108 | isMultiline = isAnyMultiline(isMultilineMap, node.key, node.equalConstant, node.value); 109 | break; 110 | 111 | // Possible for a parent to assign an isMultiline override. 112 | case Ast.NodeKind.ArrayWrapper: 113 | isMultiline = isAnyMultiline(isMultilineMap, ...node.elements); 114 | break; 115 | 116 | case Ast.NodeKind.ListExpression: 117 | case Ast.NodeKind.ListLiteral: 118 | case Ast.NodeKind.RecordExpression: 119 | case Ast.NodeKind.RecordLiteral: 120 | isMultiline = visitListOrRecordNode(node, isMultilineMap); 121 | setIsMultiline(isMultilineMap, node.content, isMultiline); 122 | break; 123 | 124 | case Ast.NodeKind.Csv: 125 | isMultiline = isAnyMultiline(isMultilineMap, node.node, node.commaConstant); 126 | break; 127 | 128 | case Ast.NodeKind.ErrorHandlingExpression: 129 | isMultiline = isAnyMultiline(isMultilineMap, node.tryConstant, node.protectedExpression, node.handler); 130 | 131 | break; 132 | 133 | case Ast.NodeKind.FieldProjection: 134 | isMultiline = isAnyMultiline( 135 | isMultilineMap, 136 | node.openWrapperConstant, 137 | node.closeWrapperConstant, 138 | node.optionalConstant, 139 | ...node.content.elements, 140 | ); 141 | 142 | break; 143 | 144 | case Ast.NodeKind.FieldSelector: 145 | isMultiline = isAnyMultiline( 146 | isMultilineMap, 147 | node.openWrapperConstant, 148 | node.content, 149 | node.closeWrapperConstant, 150 | node.optionalConstant, 151 | ); 152 | 153 | break; 154 | 155 | case Ast.NodeKind.FieldSpecification: 156 | isMultiline = isAnyMultiline(isMultilineMap, node.optionalConstant, node.name, node.fieldTypeSpecification); 157 | 158 | break; 159 | 160 | case Ast.NodeKind.FieldSpecificationList: { 161 | const fieldArray: Ast.ICsvArray = node.content; 162 | 163 | const fields: ReadonlyArray> = fieldArray.elements; 164 | 165 | if (fields.length > 1) { 166 | isMultiline = true; 167 | } else if (fields.length === 1 && node.openRecordMarkerConstant) { 168 | isMultiline = true; 169 | } 170 | 171 | setIsMultiline(isMultilineMap, fieldArray, isMultiline); 172 | break; 173 | } 174 | 175 | case Ast.NodeKind.FieldTypeSpecification: 176 | isMultiline = isAnyMultiline(isMultilineMap, node.equalConstant, node.fieldType); 177 | break; 178 | 179 | case Ast.NodeKind.FunctionExpression: 180 | isMultiline = expectGetIsMultiline(isMultilineMap, node.expression); 181 | break; 182 | 183 | case Ast.NodeKind.IdentifierExpression: { 184 | isMultiline = isAnyMultiline(isMultilineMap, node.inclusiveConstant, node.identifier); 185 | break; 186 | } 187 | 188 | case Ast.NodeKind.IfExpression: 189 | isMultiline = true; 190 | break; 191 | 192 | case Ast.NodeKind.InvokeExpression: { 193 | const nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection = state.nodeIdMapCollection; 194 | const args: ReadonlyArray> = node.content.elements; 195 | 196 | if (args.length > 1) { 197 | const linearLengthMap: LinearLengthMap = state.linearLengthMap; 198 | 199 | const linearLength: number = await getLinearLength( 200 | nodeIdMapCollection, 201 | linearLengthMap, 202 | node, 203 | state.locale, 204 | state.traceManager, 205 | trace.id, 206 | state.cancellationToken, 207 | ); 208 | 209 | const arrayWrapper: Ast.TNode | undefined = PQP.Parser.NodeIdMapUtils.parentAst( 210 | nodeIdMapCollection, 211 | node.id, 212 | ); 213 | 214 | if (arrayWrapper === undefined || arrayWrapper.kind !== Ast.NodeKind.ArrayWrapper) { 215 | throw new PQP.CommonError.InvariantError("InvokeExpression must have ArrayWrapper as a parent"); 216 | } 217 | 218 | const recursivePrimaryExpression: Ast.TNode | undefined = PQP.Parser.NodeIdMapUtils.parentAst( 219 | nodeIdMapCollection, 220 | arrayWrapper.id, 221 | ); 222 | 223 | if ( 224 | recursivePrimaryExpression === undefined || 225 | recursivePrimaryExpression.kind !== Ast.NodeKind.RecursivePrimaryExpression 226 | ) { 227 | throw new PQP.CommonError.InvariantError( 228 | "ArrayWrapper must have RecursivePrimaryExpression as a parent", 229 | ); 230 | } 231 | 232 | const headLinearLength: number = await getLinearLength( 233 | nodeIdMapCollection, 234 | linearLengthMap, 235 | recursivePrimaryExpression.head, 236 | state.locale, 237 | state.traceManager, 238 | trace.id, 239 | state.cancellationToken, 240 | ); 241 | 242 | const compositeLinearLength: number = headLinearLength + linearLength; 243 | 244 | // if it's beyond the threshold check if it's a long literal 245 | // ex. `#datetimezone(2013,02,26, 09,15,00, 09,00)` 246 | if (compositeLinearLength > InvokeExpressionLinearLengthThreshold) { 247 | const identifierLiteral: string | undefined = 248 | PQP.Parser.NodeIdMapUtils.invokeExpressionIdentifierLiteral(nodeIdMapCollection, node.id); 249 | 250 | if (identifierLiteral) { 251 | const name: string = identifierLiteral; 252 | isMultiline = InvokeExpressionIdentifierLinearLengthExclusions.indexOf(name) === -1; 253 | } 254 | 255 | setIsMultiline(isMultilineMap, node.content, isMultiline); 256 | } else { 257 | isMultiline = isAnyMultiline( 258 | isMultilineMap, 259 | node.openWrapperConstant, 260 | node.closeWrapperConstant, 261 | ...args, 262 | ); 263 | } 264 | } else { 265 | // a single argument can still be multiline 266 | // ex. `foo(if true then 1 else 0)` 267 | isMultiline = isAnyMultiline( 268 | isMultilineMap, 269 | node.openWrapperConstant, 270 | node.closeWrapperConstant, 271 | ...args, 272 | ); 273 | } 274 | 275 | break; 276 | } 277 | 278 | case Ast.NodeKind.ItemAccessExpression: 279 | isMultiline = isAnyMultiline( 280 | isMultilineMap, 281 | node.optionalConstant, 282 | node.content, 283 | node.closeWrapperConstant, 284 | node.optionalConstant, 285 | ); 286 | 287 | break; 288 | 289 | case Ast.NodeKind.LetExpression: 290 | isMultiline = true; 291 | setIsMultiline(isMultilineMap, node.variableList, true); 292 | break; 293 | 294 | case Ast.NodeKind.LiteralExpression: 295 | if (node.literalKind === Ast.LiteralKind.Text && containsNewline(node.literal)) { 296 | isMultiline = true; 297 | } 298 | 299 | break; 300 | 301 | case Ast.NodeKind.ListType: 302 | isMultiline = isAnyMultiline( 303 | isMultilineMap, 304 | node.openWrapperConstant, 305 | node.content, 306 | node.closeWrapperConstant, 307 | ); 308 | 309 | break; 310 | 311 | case Ast.NodeKind.MetadataExpression: 312 | isMultiline = isAnyMultiline(isMultilineMap, node.left, node.operatorConstant, node.right); 313 | break; 314 | 315 | case Ast.NodeKind.ParenthesizedExpression: 316 | isMultiline = isAnyMultiline( 317 | isMultilineMap, 318 | node.openWrapperConstant, 319 | node.content, 320 | node.closeWrapperConstant, 321 | ); 322 | 323 | break; 324 | 325 | case Ast.NodeKind.RangeExpression: 326 | isMultiline = isAnyMultiline(isMultilineMap, node.left, node.rangeConstant, node.right); 327 | break; 328 | 329 | case Ast.NodeKind.RecordType: 330 | isMultiline = expectGetIsMultiline(isMultilineMap, node.fields); 331 | break; 332 | 333 | case Ast.NodeKind.RecursivePrimaryExpression: 334 | isMultiline = isAnyMultiline(isMultilineMap, node.head, ...node.recursiveExpressions.elements); 335 | break; 336 | 337 | case Ast.NodeKind.Section: 338 | if (node.sectionMembers.elements.length) { 339 | isMultiline = true; 340 | } else { 341 | isMultiline = isAnyMultiline( 342 | isMultilineMap, 343 | node.literalAttributes, 344 | node.sectionConstant, 345 | node.name, 346 | node.semicolonConstant, 347 | ...node.sectionMembers.elements, 348 | ); 349 | } 350 | 351 | break; 352 | 353 | case Ast.NodeKind.SectionMember: 354 | isMultiline = isAnyMultiline( 355 | isMultilineMap, 356 | node.literalAttributes, 357 | node.sharedConstant, 358 | node.namePairedExpression, 359 | node.semicolonConstant, 360 | ); 361 | 362 | break; 363 | 364 | case Ast.NodeKind.TableType: 365 | isMultiline = isAnyMultiline(isMultilineMap, node.tableConstant, node.rowType); 366 | break; 367 | 368 | case Ast.NodeKind.UnaryExpression: 369 | isMultiline = isAnyMultiline(isMultilineMap, ...node.operators.elements); 370 | break; 371 | 372 | // no-op nodes 373 | case Ast.NodeKind.Constant: 374 | case Ast.NodeKind.FunctionType: 375 | case Ast.NodeKind.GeneralizedIdentifier: 376 | case Ast.NodeKind.Identifier: 377 | case Ast.NodeKind.NotImplementedExpression: 378 | case Ast.NodeKind.Parameter: 379 | case Ast.NodeKind.ParameterList: 380 | case Ast.NodeKind.PrimitiveType: 381 | break; 382 | 383 | default: 384 | throw PQP.Assert.isNever(node); 385 | } 386 | 387 | setIsMultilineWithCommentCheck(state, node, isMultiline); 388 | 389 | trace.exit({ isMultiline }); 390 | } 391 | 392 | async function visitBinOpExpression( 393 | state: IsMultilineFirstPassState, 394 | node: Ast.TBinOpExpression, 395 | isMultilineMap: IsMultilineMap, 396 | correlationId: number, 397 | ): Promise { 398 | const trace: Trace = state.traceManager.entry( 399 | FormatTraceConstant.IsMultilinePhase1, 400 | visitBinOpExpression.name, 401 | correlationId, 402 | ); 403 | 404 | const left: Ast.TNode = node.left; 405 | const right: Ast.TNode = node.right; 406 | 407 | let isMultiline: boolean; 408 | 409 | if ( 410 | (AstUtils.isTBinOpExpression(left) && containsLogicalExpression(left)) || 411 | (AstUtils.isTBinOpExpression(right) && containsLogicalExpression(right)) 412 | ) { 413 | isMultiline = true; 414 | } 415 | 416 | const linearLength: number = await getLinearLength( 417 | state.nodeIdMapCollection, 418 | state.linearLengthMap, 419 | node, 420 | state.locale, 421 | state.traceManager, 422 | trace.id, 423 | state.cancellationToken, 424 | ); 425 | 426 | if (linearLength > TBinOpExpressionLinearLengthThreshold) { 427 | isMultiline = true; 428 | } else { 429 | isMultiline = isAnyMultiline(isMultilineMap, left, node.operatorConstant, right); 430 | } 431 | 432 | trace.exit(); 433 | 434 | return isMultiline; 435 | } 436 | 437 | function visitListOrRecordNode( 438 | node: Ast.ListExpression | Ast.ListLiteral | Ast.RecordExpression | Ast.RecordLiteral, 439 | isMultilineMap: IsMultilineMap, 440 | ): boolean { 441 | if (node.content.elements.length > 1) { 442 | return true; 443 | } else { 444 | const isAnyChildMultiline: boolean = isAnyMultiline( 445 | isMultilineMap, 446 | node.openWrapperConstant, 447 | node.closeWrapperConstant, 448 | ...node.content.elements, 449 | ); 450 | 451 | if (isAnyChildMultiline) { 452 | return true; 453 | } else { 454 | const csvs: ReadonlyArray = node.content.elements; 455 | 456 | const csvNodes: ReadonlyArray = csvs.map((csv: Ast.TCsv) => csv.node); 457 | 458 | return isAnyListOrRecord(csvNodes); 459 | } 460 | } 461 | } 462 | 463 | function isAnyListOrRecord(nodes: ReadonlyArray): boolean { 464 | for (const node of nodes) { 465 | // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check 466 | switch (node.kind) { 467 | case Ast.NodeKind.ListExpression: 468 | case Ast.NodeKind.ListLiteral: 469 | case Ast.NodeKind.RecordExpression: 470 | case Ast.NodeKind.RecordLiteral: 471 | return true; 472 | } 473 | } 474 | 475 | return false; 476 | } 477 | 478 | function isAnyMultiline(isMultilineMap: IsMultilineMap, ...nodes: (Ast.TNode | undefined)[]): boolean { 479 | for (const node of nodes) { 480 | if (node && expectGetIsMultiline(isMultilineMap, node)) { 481 | return true; 482 | } 483 | } 484 | 485 | return false; 486 | } 487 | 488 | function setIsMultilineWithCommentCheck(state: IsMultilineFirstPassState, node: Ast.TNode, isMultiline: boolean): void { 489 | if (precededByMultilineComment(state, node)) { 490 | isMultiline = true; 491 | } 492 | 493 | setIsMultiline(state.result, node, isMultiline); 494 | } 495 | 496 | function precededByMultilineComment(state: IsMultilineFirstPassState, node: Ast.TNode): boolean { 497 | const commentCollection: CommentCollection | undefined = state.commentCollectionMap.get(node.id); 498 | 499 | if (commentCollection) { 500 | return commentCollection.prefixedCommentsContainsNewline; 501 | } else { 502 | return false; 503 | } 504 | } 505 | 506 | function containsNewline(text: string): boolean { 507 | const textLength: number = text.length; 508 | 509 | for (let index: number = 0; index < textLength; index += 1) { 510 | if (PQP.StringUtils.newlineKindAt(text, index)) { 511 | return true; 512 | } 513 | } 514 | 515 | return false; 516 | } 517 | 518 | function containsLogicalExpression(node: Ast.TBinOpExpression): boolean { 519 | if (!AstUtils.isTBinOpExpression(node)) { 520 | return false; 521 | } 522 | 523 | const left: Ast.TNode = node.left; 524 | const right: Ast.TNode = node.right; 525 | 526 | return ( 527 | node.kind === Ast.NodeKind.LogicalExpression || 528 | (AstUtils.isTBinOpExpression(left) && containsLogicalExpression(left)) || 529 | (AstUtils.isTBinOpExpression(right) && containsLogicalExpression(right)) 530 | ); 531 | } 532 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/isMultiline/isMultilineSecondPass.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Ast, AstUtils } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 6 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 7 | 8 | import { expectGetIsMultiline, setIsMultiline } from "./common"; 9 | import { IsMultilineMap, IsMultilineSecondPassState } from "../commonTypes"; 10 | import { FormatTraceConstant } from "../../trace"; 11 | 12 | export function tryTraverseIsMultilineSecondPass( 13 | ast: Ast.TNode, 14 | isMultilineMap: IsMultilineMap, 15 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 16 | locale: string, 17 | traceManager: TraceManager, 18 | correlationId: number | undefined, 19 | cancellationToken: PQP.ICancellationToken | undefined, 20 | ): Promise> { 21 | const state: IsMultilineSecondPassState = { 22 | locale, 23 | traceManager, 24 | cancellationToken, 25 | initialCorrelationId: correlationId, 26 | nodeIdMapCollection, 27 | result: isMultilineMap, 28 | }; 29 | 30 | return PQP.Traverse.tryTraverseAst( 31 | state, 32 | nodeIdMapCollection, 33 | ast, 34 | PQP.Traverse.VisitNodeStrategy.BreadthFirst, 35 | visitNode, 36 | PQP.Traverse.assertGetAllAstChildren, 37 | undefined, 38 | ); 39 | } 40 | 41 | // eslint-disable-next-line require-await 42 | async function visitNode( 43 | state: IsMultilineSecondPassState, 44 | node: Ast.TNode, 45 | correlationId: number | undefined, 46 | ): Promise { 47 | const trace: Trace = state.traceManager.entry( 48 | FormatTraceConstant.IsMultilinePhase2, 49 | visitNode.name, 50 | correlationId, 51 | { 52 | nodeId: node.id, 53 | nodeKind: node.kind, 54 | }, 55 | ); 56 | 57 | // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check 58 | switch (node.kind) { 59 | // TBinOpExpression 60 | case Ast.NodeKind.ArithmeticExpression: 61 | case Ast.NodeKind.AsExpression: 62 | case Ast.NodeKind.EqualityExpression: 63 | case Ast.NodeKind.IsExpression: 64 | case Ast.NodeKind.LogicalExpression: 65 | case Ast.NodeKind.RelationalExpression: 66 | visitBinOpExpression(state, node, trace); 67 | break; 68 | 69 | // If a list or record is a child node, 70 | // Then by default it should be considered multiline if it has one or more values 71 | case Ast.NodeKind.ListExpression: 72 | case Ast.NodeKind.ListLiteral: 73 | case Ast.NodeKind.RecordExpression: 74 | case Ast.NodeKind.RecordLiteral: 75 | visitListOrRecord(state, node, trace); 76 | } 77 | 78 | trace.exit(); 79 | } 80 | 81 | function visitBinOpExpression(state: IsMultilineSecondPassState, node: Ast.TNode, trace: Trace): void { 82 | const isMultilineMap: IsMultilineMap = state.result; 83 | const parent: Ast.TNode | undefined = PQP.Parser.NodeIdMapUtils.parentAst(state.nodeIdMapCollection, node.id); 84 | 85 | if (parent && AstUtils.isTBinOpExpression(parent) && expectGetIsMultiline(isMultilineMap, parent)) { 86 | trace.trace("Updating isMultiline for nested BinOp", { nodeId: node.id, nodeKind: node.kind }); 87 | 88 | setIsMultiline(isMultilineMap, node, true); 89 | } 90 | } 91 | 92 | function visitListOrRecord( 93 | state: IsMultilineSecondPassState, 94 | node: Ast.ListExpression | Ast.ListLiteral | Ast.RecordExpression | Ast.RecordLiteral, 95 | trace: Trace, 96 | ): void { 97 | if (node.content.elements.length) { 98 | trace.trace("Updating isMultiline for collection"); 99 | 100 | const nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection = state.nodeIdMapCollection; 101 | 102 | let parent: Ast.TNode | undefined = PQP.Parser.NodeIdMapUtils.parentAst(nodeIdMapCollection, node.id); 103 | let csv: Ast.TCsv | undefined; 104 | let arrayWrapper: Ast.TArrayWrapper | undefined; 105 | 106 | if (parent && parent.kind === Ast.NodeKind.Csv) { 107 | csv = parent; 108 | parent = PQP.Parser.NodeIdMapUtils.parentAst(nodeIdMapCollection, parent.id); 109 | } 110 | 111 | if (parent && parent.kind === Ast.NodeKind.ArrayWrapper) { 112 | arrayWrapper = parent; 113 | parent = PQP.Parser.NodeIdMapUtils.parentAst(nodeIdMapCollection, parent.id); 114 | } 115 | 116 | if (parent) { 117 | switch (parent.kind) { 118 | case Ast.NodeKind.ItemAccessExpression: 119 | case Ast.NodeKind.InvokeExpression: 120 | case Ast.NodeKind.FunctionExpression: 121 | case Ast.NodeKind.Section: 122 | case Ast.NodeKind.SectionMember: 123 | break; 124 | 125 | default: { 126 | const isMultilineMap: IsMultilineMap = state.result; 127 | setIsMultiline(isMultilineMap, parent, true); 128 | 129 | if (csv) { 130 | setIsMultiline(isMultilineMap, csv, true); 131 | } 132 | 133 | if (arrayWrapper) { 134 | setIsMultiline(isMultilineMap, arrayWrapper, true); 135 | } 136 | 137 | setIsMultiline(isMultilineMap, node, true); 138 | setIsMultiline(isMultilineMap, node.content, true); 139 | } 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/isMultiline/linearLength.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 6 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 7 | 8 | import { LinearLengthMap, LinearLengthState } from "../commonTypes"; 9 | import { FormatTraceConstant } from "../../trace"; 10 | 11 | // Lazy evaluation of a potentially large AST. 12 | // Returns the length of text if the node was formatted on a single line. 13 | // 14 | // Eg. the linear length of `{1, 2, 3}` as an Ast would give 9. 15 | // 16 | // Some nodes are always multiline, such as IfExpression, and will return NaN. 17 | export async function getLinearLength( 18 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 19 | linearLengthMap: LinearLengthMap, 20 | node: Ast.TNode, 21 | locale: string, 22 | traceManager: TraceManager, 23 | correlationId: number | undefined, 24 | cancellationToken: PQP.ICancellationToken | undefined, 25 | ): Promise { 26 | const nodeId: number = node.id; 27 | const linearLength: number | undefined = linearLengthMap.get(nodeId); 28 | 29 | if (linearLength === undefined) { 30 | const linearLength: number = await calculateLinearLength( 31 | nodeIdMapCollection, 32 | linearLengthMap, 33 | node, 34 | locale, 35 | traceManager, 36 | correlationId, 37 | cancellationToken, 38 | ); 39 | 40 | linearLengthMap.set(nodeId, linearLength); 41 | 42 | return linearLength; 43 | } else { 44 | return linearLength; 45 | } 46 | } 47 | 48 | async function calculateLinearLength( 49 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 50 | linearLengthMap: LinearLengthMap, 51 | node: Ast.TNode, 52 | locale: string, 53 | traceManager: TraceManager, 54 | correlationId: number | undefined, 55 | cancellationToken: PQP.ICancellationToken | undefined, 56 | ): Promise { 57 | const state: LinearLengthState = { 58 | locale, 59 | traceManager, 60 | cancellationToken, 61 | initialCorrelationId: correlationId, 62 | linearLengthMap, 63 | nodeIdMapCollection, 64 | result: 0, 65 | }; 66 | 67 | const triedTraverse: PQP.Traverse.TriedTraverse = await PQP.Traverse.tryTraverseAst( 68 | state, 69 | nodeIdMapCollection, 70 | node, 71 | PQP.Traverse.VisitNodeStrategy.DepthFirst, 72 | visitNode, 73 | PQP.Traverse.assertGetAllAstChildren, 74 | undefined, 75 | ); 76 | 77 | if (PQP.ResultUtils.isError(triedTraverse)) { 78 | throw triedTraverse.error; 79 | } else { 80 | return triedTraverse.value; 81 | } 82 | } 83 | 84 | async function visitNode(state: LinearLengthState, node: Ast.TNode, correlationId: number | undefined): Promise { 85 | const trace: Trace = state.traceManager.entry(FormatTraceConstant.LinearLength, visitNode.name, correlationId, { 86 | nodeId: node.id, 87 | nodeKind: node.kind, 88 | }); 89 | 90 | let linearLength: number; 91 | 92 | switch (node.kind) { 93 | // TPairedConstant 94 | case Ast.NodeKind.AsNullablePrimitiveType: 95 | case Ast.NodeKind.AsType: 96 | case Ast.NodeKind.CatchExpression: 97 | case Ast.NodeKind.EachExpression: 98 | case Ast.NodeKind.ErrorRaisingExpression: 99 | case Ast.NodeKind.IsNullablePrimitiveType: 100 | case Ast.NodeKind.NullablePrimitiveType: 101 | case Ast.NodeKind.NullableType: 102 | case Ast.NodeKind.OtherwiseExpression: 103 | case Ast.NodeKind.TypePrimaryType: 104 | linearLength = await sumLinearLengths(state, trace.id, 1, node.constant, node.paired); 105 | break; 106 | 107 | // TBinOpExpression 108 | case Ast.NodeKind.ArithmeticExpression: 109 | case Ast.NodeKind.AsExpression: 110 | case Ast.NodeKind.EqualityExpression: 111 | case Ast.NodeKind.IsExpression: 112 | case Ast.NodeKind.LogicalExpression: 113 | case Ast.NodeKind.NullCoalescingExpression: 114 | case Ast.NodeKind.RelationalExpression: 115 | linearLength = await visitBinOpExpressionNode(state, node, trace.id); 116 | break; 117 | 118 | // TKeyValuePair 119 | case Ast.NodeKind.GeneralizedIdentifierPairedAnyLiteral: 120 | case Ast.NodeKind.GeneralizedIdentifierPairedExpression: 121 | case Ast.NodeKind.IdentifierPairedExpression: 122 | linearLength = await sumLinearLengths(state, trace.id, 2, node.key, node.equalConstant, node.value); 123 | break; 124 | 125 | // TWrapped where Content is TCsv[] and no extra attributes 126 | case Ast.NodeKind.InvokeExpression: 127 | case Ast.NodeKind.ListExpression: 128 | case Ast.NodeKind.ListLiteral: 129 | case Ast.NodeKind.ParameterList: 130 | case Ast.NodeKind.RecordExpression: 131 | case Ast.NodeKind.RecordLiteral: 132 | linearLength = await visitWrappedCsvArray(state, node, trace.id); 133 | break; 134 | 135 | case Ast.NodeKind.ArrayWrapper: 136 | linearLength = await sumLinearLengths(state, trace.id, 0, ...node.elements); 137 | break; 138 | 139 | case Ast.NodeKind.Constant: 140 | linearLength = node.constantKind.length; 141 | break; 142 | 143 | case Ast.NodeKind.Csv: 144 | linearLength = await sumLinearLengths(state, trace.id, 0, node.node, node.commaConstant); 145 | break; 146 | 147 | case Ast.NodeKind.ErrorHandlingExpression: { 148 | let initialLength: number = 1; 149 | 150 | if (node.handler) { 151 | initialLength += 2; 152 | } 153 | 154 | linearLength = await sumLinearLengths( 155 | state, 156 | trace.id, 157 | initialLength, 158 | node.tryConstant, 159 | node.protectedExpression, 160 | node.handler, 161 | ); 162 | 163 | break; 164 | } 165 | 166 | case Ast.NodeKind.FieldProjection: 167 | linearLength = await sumLinearLengths( 168 | state, 169 | trace.id, 170 | 0, 171 | node.openWrapperConstant, 172 | node.closeWrapperConstant, 173 | node.optionalConstant, 174 | ...node.content.elements, 175 | ); 176 | 177 | break; 178 | 179 | case Ast.NodeKind.FieldSelector: 180 | linearLength = await sumLinearLengths( 181 | state, 182 | trace.id, 183 | 0, 184 | node.openWrapperConstant, 185 | node.content, 186 | node.closeWrapperConstant, 187 | node.optionalConstant, 188 | ); 189 | 190 | break; 191 | 192 | case Ast.NodeKind.FieldSpecification: 193 | linearLength = await sumLinearLengths( 194 | state, 195 | trace.id, 196 | 0, 197 | node.optionalConstant, 198 | node.name, 199 | node.fieldTypeSpecification, 200 | ); 201 | 202 | break; 203 | 204 | case Ast.NodeKind.FieldSpecificationList: { 205 | const elements: ReadonlyArray> = node.content.elements; 206 | 207 | let initialLength: number = 0; 208 | 209 | if (node.openRecordMarkerConstant && elements.length) { 210 | initialLength += 2; 211 | } 212 | 213 | linearLength = await sumLinearLengths( 214 | state, 215 | trace.id, 216 | initialLength, 217 | node.openWrapperConstant, 218 | node.closeWrapperConstant, 219 | node.openRecordMarkerConstant, 220 | ...elements, 221 | ); 222 | 223 | break; 224 | } 225 | 226 | case Ast.NodeKind.FieldTypeSpecification: 227 | linearLength = await sumLinearLengths(state, trace.id, 2, node.equalConstant, node.fieldType); 228 | break; 229 | 230 | case Ast.NodeKind.FunctionExpression: { 231 | let initialLength: number = 2; 232 | 233 | if (node.functionReturnType) { 234 | initialLength += 2; 235 | } 236 | 237 | linearLength = await sumLinearLengths( 238 | state, 239 | trace.id, 240 | initialLength, 241 | node.parameters, 242 | node.functionReturnType, 243 | node.fatArrowConstant, 244 | node.expression, 245 | ); 246 | 247 | break; 248 | } 249 | 250 | case Ast.NodeKind.FunctionType: 251 | linearLength = await sumLinearLengths( 252 | state, 253 | trace.id, 254 | 2, 255 | node.functionConstant, 256 | node.parameters, 257 | node.functionReturnType, 258 | ); 259 | 260 | break; 261 | 262 | case Ast.NodeKind.GeneralizedIdentifier: 263 | case Ast.NodeKind.Identifier: 264 | linearLength = node.literal.length; 265 | break; 266 | 267 | case Ast.NodeKind.IdentifierExpression: 268 | linearLength = await sumLinearLengths(state, trace.id, 0, node.inclusiveConstant, node.identifier); 269 | break; 270 | 271 | case Ast.NodeKind.ItemAccessExpression: 272 | linearLength = await sumLinearLengths( 273 | state, 274 | trace.id, 275 | 0, 276 | node.openWrapperConstant, 277 | node.content, 278 | node.closeWrapperConstant, 279 | node.optionalConstant, 280 | ); 281 | 282 | break; 283 | 284 | case Ast.NodeKind.LiteralExpression: 285 | linearLength = node.literal.length; 286 | break; 287 | 288 | case Ast.NodeKind.ListType: 289 | linearLength = await sumLinearLengths( 290 | state, 291 | trace.id, 292 | 0, 293 | node.openWrapperConstant, 294 | node.content, 295 | node.closeWrapperConstant, 296 | ); 297 | 298 | break; 299 | 300 | case Ast.NodeKind.MetadataExpression: { 301 | linearLength = await sumLinearLengths(state, trace.id, 2, node.left, node.operatorConstant, node.right); 302 | break; 303 | } 304 | 305 | case Ast.NodeKind.NotImplementedExpression: 306 | linearLength = await sumLinearLengths(state, trace.id, 0, node.ellipsisConstant); 307 | break; 308 | 309 | case Ast.NodeKind.Parameter: { 310 | let initialLength: number = 0; 311 | 312 | if (node.optionalConstant) { 313 | initialLength += 1; 314 | } 315 | 316 | if (node.parameterType) { 317 | initialLength += 1; 318 | } 319 | 320 | linearLength = await sumLinearLengths( 321 | state, 322 | trace.id, 323 | initialLength, 324 | node.optionalConstant, 325 | node.name, 326 | node.parameterType, 327 | ); 328 | 329 | break; 330 | } 331 | 332 | case Ast.NodeKind.ParenthesizedExpression: 333 | linearLength = await sumLinearLengths( 334 | state, 335 | trace.id, 336 | 0, 337 | node.openWrapperConstant, 338 | node.content, 339 | node.closeWrapperConstant, 340 | ); 341 | 342 | break; 343 | 344 | case Ast.NodeKind.PrimitiveType: 345 | linearLength = node.primitiveTypeKind.length; 346 | break; 347 | 348 | case Ast.NodeKind.RangeExpression: 349 | linearLength = await sumLinearLengths(state, trace.id, 0, node.left, node.rangeConstant, node.right); 350 | break; 351 | 352 | case Ast.NodeKind.RecordType: 353 | linearLength = await sumLinearLengths(state, trace.id, 0, node.fields); 354 | break; 355 | 356 | case Ast.NodeKind.RecursivePrimaryExpression: 357 | linearLength = await sumLinearLengths(state, trace.id, 0, node.head, ...node.recursiveExpressions.elements); 358 | break; 359 | 360 | case Ast.NodeKind.SectionMember: { 361 | let initialLength: number = 0; 362 | 363 | if (node.literalAttributes) { 364 | initialLength += 1; 365 | } 366 | 367 | if (node.sharedConstant) { 368 | initialLength += 1; 369 | } 370 | 371 | linearLength = await sumLinearLengths( 372 | state, 373 | trace.id, 374 | initialLength, 375 | node.literalAttributes, 376 | node.sharedConstant, 377 | node.namePairedExpression, 378 | node.semicolonConstant, 379 | ); 380 | 381 | break; 382 | } 383 | 384 | case Ast.NodeKind.Section: { 385 | const sectionMembers: ReadonlyArray = node.sectionMembers.elements; 386 | 387 | if (sectionMembers.length) { 388 | linearLength = NaN; 389 | } else { 390 | let initialLength: number = 0; 391 | 392 | if (node.literalAttributes) { 393 | initialLength += 1; 394 | } 395 | 396 | if (node.name) { 397 | initialLength += 1; 398 | } 399 | 400 | linearLength = await sumLinearLengths( 401 | state, 402 | trace.id, 403 | initialLength, 404 | node.literalAttributes, 405 | node.sectionConstant, 406 | node.name, 407 | node.semicolonConstant, 408 | ...sectionMembers, 409 | ); 410 | } 411 | 412 | break; 413 | } 414 | 415 | case Ast.NodeKind.TableType: 416 | linearLength = await sumLinearLengths(state, trace.id, 1, node.tableConstant, node.rowType); 417 | break; 418 | 419 | case Ast.NodeKind.UnaryExpression: 420 | linearLength = await sumLinearLengths(state, trace.id, 1, node.typeExpression, ...node.operators.elements); 421 | break; 422 | 423 | // is always multiline, therefore cannot have linear line length 424 | case Ast.NodeKind.IfExpression: 425 | case Ast.NodeKind.LetExpression: 426 | linearLength = NaN; 427 | break; 428 | 429 | default: 430 | throw PQP.Assert.isNever(node); 431 | } 432 | 433 | state.linearLengthMap.set(node.id, linearLength); 434 | state.result = linearLength; 435 | 436 | trace.exit({ linearLength }); 437 | } 438 | 439 | // eslint-disable-next-line require-await 440 | async function visitBinOpExpressionNode( 441 | state: LinearLengthState, 442 | node: Ast.TBinOpExpression, 443 | correlationId: number | undefined, 444 | ): Promise { 445 | return sumLinearLengths( 446 | state, 447 | correlationId, 448 | node.operatorConstant.constantKind.length, 449 | node.left, 450 | node.operatorConstant, 451 | node.right, 452 | ); 453 | } 454 | 455 | function visitWrappedCsvArray( 456 | state: LinearLengthState, 457 | node: 458 | | Ast.InvokeExpression 459 | | Ast.ListExpression 460 | | Ast.ListLiteral 461 | | Ast.TParameterList 462 | | Ast.RecordExpression 463 | | Ast.RecordLiteral, 464 | correlationId: number | undefined, 465 | ): Promise { 466 | const elements: ReadonlyArray = node.content.elements; 467 | const numElements: number = elements.length; 468 | 469 | return sumLinearLengths( 470 | state, 471 | correlationId, 472 | numElements ? numElements - 1 : 0, 473 | node.openWrapperConstant, 474 | node.closeWrapperConstant, 475 | ...elements, 476 | ); 477 | } 478 | 479 | async function sumLinearLengths( 480 | state: LinearLengthState, 481 | correlationId: number | undefined, 482 | initialLength: number, 483 | ...nodes: (Ast.TNode | undefined)[] 484 | ): Promise { 485 | const trace: Trace = state.traceManager.entry( 486 | FormatTraceConstant.LinearLength, 487 | sumLinearLengths.name, 488 | correlationId, 489 | ); 490 | 491 | const filteredNodes: Ast.TNode[] = nodes.filter( 492 | (value: Ast.TNode | undefined): value is Ast.TNode => value !== undefined, 493 | ); 494 | 495 | const linearLengths: ReadonlyArray = await PQP.ArrayUtils.mapAsync(filteredNodes, (node: Ast.TNode) => 496 | getLinearLength( 497 | state.nodeIdMapCollection, 498 | state.linearLengthMap, 499 | node, 500 | state.locale, 501 | state.traceManager, 502 | trace.id, 503 | state.cancellationToken, 504 | ), 505 | ); 506 | 507 | const result: number = linearLengths.reduce( 508 | (sum: number, linearLength: number) => sum + linearLength, 509 | initialLength, 510 | ); 511 | 512 | trace.exit(); 513 | 514 | return result; 515 | } 516 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/preProcessParameter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | 6 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 7 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 8 | 9 | import { CommentCollectionMap, SerializeParameter, SerializeParameterMap } from "./commonTypes"; 10 | import { FormatTraceConstant } from "../trace"; 11 | import { getNodeScopeName } from "../themes"; 12 | import { NodeKind } from "@microsoft/powerquery-parser/lib/powerquery-parser/language/ast/ast"; 13 | 14 | interface PreProcessState extends PQP.Traverse.ITraversalState { 15 | readonly nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection; 16 | readonly commentCollectionMap: CommentCollectionMap; 17 | readonly cancellationToken: PQP.ICancellationToken | undefined; 18 | currentNode: Ast.TNode; 19 | readonly visitedNodes: Ast.TNode[]; 20 | readonly visitedNodeScopeNames: string[]; 21 | readonly result: SerializeParameterMap; 22 | } 23 | 24 | type ParameterPreProcessor = (state: PreProcessState) => void; 25 | 26 | function parameterizeOneNodeInForceBlockMode( 27 | currentNode: Ast.TNode, 28 | serializeParameterMap: Map, 29 | ): void { 30 | let nullableCurrentNodeSerializeParameter: SerializeParameter | undefined = serializeParameterMap.get( 31 | currentNode.id, 32 | ); 33 | 34 | // we need to do immutable modification over here and also need to wrap it in the following preprocessor api 35 | nullableCurrentNodeSerializeParameter = nullableCurrentNodeSerializeParameter 36 | ? { ...nullableCurrentNodeSerializeParameter } 37 | : {}; 38 | 39 | nullableCurrentNodeSerializeParameter.forceBlockMode = true; 40 | 41 | serializeParameterMap.set(currentNode.id, nullableCurrentNodeSerializeParameter); 42 | } 43 | 44 | // I was thinking, one day, we could expose another preProcessor api to allow other to register other preProcessor, and 45 | // it could help customize more serialization feature which could not be achieved via config-driven patterns 46 | // thus for now, I supposed let's put this, currently only, one preProcessor over here, and 47 | // latter find it a better place when we got those preProcessor registration apis in the following prs 48 | const recordExpressionParameterPreProcessor: ParameterPreProcessor = (state: PreProcessState) => { 49 | const currentNode: Ast.RecordExpression | Ast.RecordLiteral = state.currentNode as 50 | | Ast.RecordExpression 51 | | Ast.RecordLiteral; 52 | 53 | const serializeParameterMap: Map = state.result.parametersMap; 54 | 55 | // is current record already in different lines 56 | let shouldSerializeInMultipleLines: boolean = 57 | currentNode.openWrapperConstant.tokenRange.positionEnd.lineNumber < 58 | currentNode.closeWrapperConstant.tokenRange.positionStart.lineNumber; 59 | 60 | // is current record directly owning other records 61 | shouldSerializeInMultipleLines = 62 | shouldSerializeInMultipleLines || 63 | currentNode.content.elements.some( 64 | ( 65 | generalizedIdentifierPairedExpressionICsv: 66 | | Ast.ICsv 67 | | Ast.ICsv, 68 | ): boolean => generalizedIdentifierPairedExpressionICsv.node.value.kind === NodeKind.RecordExpression, 69 | ); 70 | 71 | if (shouldSerializeInMultipleLines) { 72 | // we got line feeds within the current record we cannot serialize it into the in-line record 73 | parameterizeOneNodeInForceBlockMode(currentNode, serializeParameterMap); 74 | parameterizeOneNodeInForceBlockMode(currentNode.content, serializeParameterMap); 75 | } 76 | }; 77 | 78 | const preProcessorDictionary: Map> = new Map([ 79 | [Ast.NodeKind.RecordLiteral, [recordExpressionParameterPreProcessor]], 80 | [Ast.NodeKind.RecordExpression, [recordExpressionParameterPreProcessor]], 81 | ]); 82 | 83 | export function tryPreProcessParameter( 84 | ast: Ast.TNode, 85 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 86 | commentCollectionMap: CommentCollectionMap, 87 | traceManager: TraceManager, 88 | locale: string, 89 | correlationId: number | undefined, 90 | cancellationToken: PQP.ICancellationToken | undefined, 91 | serializeParameterMap: SerializeParameterMap, 92 | ): Promise> { 93 | const trace: Trace = traceManager.entry( 94 | FormatTraceConstant.PreProcessParameter, 95 | tryPreProcessParameter.name, 96 | correlationId, 97 | ); 98 | 99 | const state: PreProcessState = { 100 | locale, 101 | traceManager, 102 | cancellationToken, 103 | initialCorrelationId: trace.id, 104 | nodeIdMapCollection, 105 | commentCollectionMap, 106 | currentNode: ast, 107 | visitedNodes: [ast], 108 | visitedNodeScopeNames: [ast.kind], 109 | result: serializeParameterMap, 110 | }; 111 | 112 | const result: Promise> = PQP.ResultUtils.ensureResultAsync( 113 | async () => { 114 | if (preProcessorDictionary.size) { 115 | await doInvokePreProcessor(state); 116 | } 117 | 118 | return state.result; 119 | }, 120 | locale, 121 | ); 122 | 123 | trace.exit(); 124 | 125 | return result; 126 | } 127 | 128 | export async function doInvokePreProcessor(state: PreProcessState): Promise { 129 | state.cancellationToken?.throwIfCancelled(); 130 | 131 | for (const child of await PQP.Traverse.assertGetAllAstChildren( 132 | state, 133 | state.currentNode, 134 | state.nodeIdMapCollection, 135 | )) { 136 | const childScopeName: string = getNodeScopeName(child); 137 | state.currentNode = child; 138 | state.visitedNodes.push(child); 139 | state.visitedNodeScopeNames.push(childScopeName); 140 | 141 | // invoke child preProcessor if any 142 | const nullablePreProcessorArray: ReadonlyArray | undefined = preProcessorDictionary.get( 143 | child.kind, 144 | ); 145 | 146 | if (Array.isArray(nullablePreProcessorArray)) { 147 | const preProcessorState: PreProcessState = { 148 | ...state, 149 | visitedNodes: state.visitedNodes.slice(), 150 | visitedNodeScopeNames: state.visitedNodeScopeNames.slice(), 151 | }; 152 | 153 | nullablePreProcessorArray.forEach((preProcessor: ParameterPreProcessor) => { 154 | preProcessor(preProcessorState); 155 | }); 156 | } 157 | 158 | // eslint-disable-next-line no-await-in-loop 159 | await doInvokePreProcessor(state); 160 | state.visitedNodeScopeNames.pop(); 161 | state.visitedNodes.pop(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/serializeParameter.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 6 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 7 | import { NodeIdMap } from "@microsoft/powerquery-parser/lib/powerquery-parser/parser"; 8 | 9 | import { 10 | CommentCollectionMap, 11 | SerializeParameter, 12 | SerializeParameterMap, 13 | SerializeParameterState, 14 | } from "./commonTypes"; 15 | import { getNodeScopeName, ScopeListElement, ScopeMetadata, ScopeMetadataProvider, StackElement } from "../themes"; 16 | import { FormatTraceConstant } from "../trace"; 17 | 18 | type RealSerializeParameterState = SerializeParameterState & { 19 | currentScopeStack: StackElement; 20 | scopeMetadataProvider: ScopeMetadataProvider; 21 | }; 22 | 23 | export function tryTraverseSerializeParameter( 24 | ast: Ast.TNode, 25 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 26 | commentCollectionMap: CommentCollectionMap, 27 | scopeMetadataProvider: ScopeMetadataProvider, 28 | locale: string, 29 | traceManager: TraceManager, 30 | correlationId: number | undefined, 31 | cancellationToken: PQP.ICancellationToken | undefined, 32 | ): Promise> { 33 | const trace: Trace = traceManager.entry( 34 | FormatTraceConstant.SerializeParameter, 35 | tryTraverseSerializeParameter.name, 36 | correlationId, 37 | ); 38 | 39 | const defaultMeta: ScopeMetadata = scopeMetadataProvider.getDefaultMetadata(); 40 | const rootScopeName: string = getNodeScopeName(ast); 41 | const rawRootMeta: ScopeMetadata = scopeMetadataProvider.getMetadataForScope(rootScopeName); 42 | 43 | const rawRootParameter: SerializeParameter = ScopeListElement.mergeParameters( 44 | defaultMeta.themeData?.[0].parameters ?? {}, 45 | undefined, 46 | rawRootMeta, 47 | ); 48 | 49 | const rootScopeList: ScopeListElement = new ScopeListElement( 50 | undefined, 51 | rootScopeName, 52 | rawRootParameter, 53 | ); 54 | 55 | const rootState: StackElement = new StackElement( 56 | undefined, 57 | ast, 58 | rootScopeList, 59 | ); 60 | 61 | const state: RealSerializeParameterState = { 62 | locale, 63 | traceManager, 64 | cancellationToken, 65 | initialCorrelationId: trace.id, 66 | commentCollectionMap, 67 | nodeIdMapCollection, 68 | currentScopeStack: rootState, 69 | scopeMetadataProvider, 70 | result: { 71 | parametersMap: new Map(), 72 | }, 73 | }; 74 | 75 | const result: Promise> = PQP.ResultUtils.ensureResultAsync( 76 | async () => { 77 | await doTraverseRecursion(state, nodeIdMapCollection, ast); 78 | 79 | return state.result; 80 | }, 81 | state.locale, 82 | ); 83 | 84 | trace.exit(); 85 | 86 | return result; 87 | } 88 | 89 | async function doTraverseRecursion( 90 | state: RealSerializeParameterState, 91 | nodeIdMapCollection: NodeIdMap.Collection, 92 | node: Ast.TNode, 93 | ): Promise { 94 | state.cancellationToken?.throwIfCancelled(); 95 | const currentScopeStack: StackElement = state.currentScopeStack; 96 | state.result.parametersMap.set(node.id, currentScopeStack.scopeList.parameters); 97 | 98 | for (const child of await PQP.Traverse.assertGetAllAstChildren(state, node, nodeIdMapCollection)) { 99 | const childScopeName: string = getNodeScopeName(child); 100 | 101 | state.currentScopeStack = state.currentScopeStack.push( 102 | child, 103 | childScopeName ? currentScopeStack.scopeList.push(state.scopeMetadataProvider, childScopeName) : undefined, 104 | ); 105 | 106 | // eslint-disable-next-line no-await-in-loop 107 | await doTraverseRecursion(state, nodeIdMapCollection, child); 108 | 109 | state.currentScopeStack = state.currentScopeStack.pop(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/powerquery-formatter/passes/utils/linearLength.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { Trace, TraceManager } from "@microsoft/powerquery-parser/lib/powerquery-parser/common/trace"; 6 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 7 | 8 | import { LinearLengthMap, LinearLengthState } from "../commonTypes"; 9 | import { FormatTraceConstant } from "../../trace"; 10 | 11 | // Lazy evaluation of a potentially large AST. 12 | // Returns the length of text if the node was formatted on a single line. 13 | // 14 | // Eg. the linear length of `{1, 2, 3}` as an Ast would give 9. 15 | // 16 | // Some nodes are always multiline, such as IfExpression, and will return NaN. 17 | export async function getLinearLength( 18 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 19 | linearLengthMap: LinearLengthMap, 20 | node: Ast.TNode, 21 | locale: string, 22 | traceManager: TraceManager, 23 | correlationId: number | undefined, 24 | cancellationToken: PQP.ICancellationToken | undefined, 25 | ): Promise { 26 | const nodeId: number = node.id; 27 | const linearLength: number | undefined = linearLengthMap.get(nodeId); 28 | 29 | if (linearLength === undefined) { 30 | const linearLength: number = await calculateLinearLength( 31 | nodeIdMapCollection, 32 | linearLengthMap, 33 | node, 34 | locale, 35 | traceManager, 36 | correlationId, 37 | cancellationToken, 38 | ); 39 | 40 | linearLengthMap.set(nodeId, linearLength); 41 | 42 | return linearLength; 43 | } else { 44 | return linearLength; 45 | } 46 | } 47 | 48 | async function calculateLinearLength( 49 | nodeIdMapCollection: PQP.Parser.NodeIdMap.Collection, 50 | linearLengthMap: LinearLengthMap, 51 | node: Ast.TNode, 52 | locale: string, 53 | traceManager: TraceManager, 54 | correlationId: number | undefined, 55 | cancellationToken: PQP.ICancellationToken | undefined, 56 | ): Promise { 57 | const state: LinearLengthState = { 58 | locale, 59 | traceManager, 60 | cancellationToken, 61 | initialCorrelationId: correlationId, 62 | linearLengthMap, 63 | nodeIdMapCollection, 64 | result: 0, 65 | }; 66 | 67 | const triedTraverse: PQP.Traverse.TriedTraverse = await PQP.Traverse.tryTraverseAst( 68 | state, 69 | nodeIdMapCollection, 70 | node, 71 | PQP.Traverse.VisitNodeStrategy.DepthFirst, 72 | visitNode, 73 | PQP.Traverse.assertGetAllAstChildren, 74 | undefined, 75 | ); 76 | 77 | if (PQP.ResultUtils.isError(triedTraverse)) { 78 | throw triedTraverse.error; 79 | } else { 80 | return triedTraverse.value; 81 | } 82 | } 83 | 84 | async function visitNode(state: LinearLengthState, node: Ast.TNode, correlationId: number | undefined): Promise { 85 | const trace: Trace = state.traceManager.entry(FormatTraceConstant.LinearLength, visitNode.name, correlationId, { 86 | nodeId: node.id, 87 | nodeKind: node.kind, 88 | }); 89 | 90 | let linearLength: number; 91 | 92 | switch (node.kind) { 93 | // TPairedConstant 94 | case Ast.NodeKind.AsNullablePrimitiveType: 95 | case Ast.NodeKind.AsType: 96 | case Ast.NodeKind.CatchExpression: 97 | case Ast.NodeKind.EachExpression: 98 | case Ast.NodeKind.ErrorRaisingExpression: 99 | case Ast.NodeKind.IsNullablePrimitiveType: 100 | case Ast.NodeKind.NullablePrimitiveType: 101 | case Ast.NodeKind.NullableType: 102 | case Ast.NodeKind.OtherwiseExpression: 103 | case Ast.NodeKind.TypePrimaryType: 104 | linearLength = await sumLinearLengths(state, trace.id, 1, node.constant, node.paired); 105 | break; 106 | 107 | // TBinOpExpression 108 | case Ast.NodeKind.ArithmeticExpression: 109 | case Ast.NodeKind.AsExpression: 110 | case Ast.NodeKind.EqualityExpression: 111 | case Ast.NodeKind.IsExpression: 112 | case Ast.NodeKind.LogicalExpression: 113 | case Ast.NodeKind.NullCoalescingExpression: 114 | case Ast.NodeKind.RelationalExpression: 115 | linearLength = await visitBinOpExpressionNode(state, node, trace.id); 116 | break; 117 | 118 | // TKeyValuePair 119 | case Ast.NodeKind.GeneralizedIdentifierPairedAnyLiteral: 120 | case Ast.NodeKind.GeneralizedIdentifierPairedExpression: 121 | case Ast.NodeKind.IdentifierPairedExpression: 122 | linearLength = await sumLinearLengths(state, trace.id, 2, node.key, node.equalConstant, node.value); 123 | break; 124 | 125 | // TWrapped where Content is TCsv[] and no extra attributes 126 | case Ast.NodeKind.InvokeExpression: 127 | case Ast.NodeKind.ListExpression: 128 | case Ast.NodeKind.ListLiteral: 129 | case Ast.NodeKind.ParameterList: 130 | case Ast.NodeKind.RecordExpression: 131 | case Ast.NodeKind.RecordLiteral: 132 | linearLength = await visitWrappedCsvArray(state, node, trace.id); 133 | break; 134 | 135 | case Ast.NodeKind.ArrayWrapper: 136 | linearLength = await sumLinearLengths(state, trace.id, 0, ...node.elements); 137 | break; 138 | 139 | case Ast.NodeKind.Constant: 140 | linearLength = node.constantKind.length; 141 | break; 142 | 143 | case Ast.NodeKind.Csv: 144 | linearLength = await sumLinearLengths(state, trace.id, 0, node.node, node.commaConstant); 145 | break; 146 | 147 | case Ast.NodeKind.ErrorHandlingExpression: { 148 | let initialLength: number = 1; 149 | 150 | if (node.handler) { 151 | initialLength += 2; 152 | } 153 | 154 | linearLength = await sumLinearLengths( 155 | state, 156 | trace.id, 157 | initialLength, 158 | node.tryConstant, 159 | node.protectedExpression, 160 | node.handler, 161 | ); 162 | 163 | break; 164 | } 165 | 166 | case Ast.NodeKind.FieldProjection: 167 | linearLength = await sumLinearLengths( 168 | state, 169 | trace.id, 170 | 0, 171 | node.openWrapperConstant, 172 | node.closeWrapperConstant, 173 | node.optionalConstant, 174 | ...node.content.elements, 175 | ); 176 | 177 | break; 178 | 179 | case Ast.NodeKind.FieldSelector: 180 | linearLength = await sumLinearLengths( 181 | state, 182 | trace.id, 183 | 0, 184 | node.openWrapperConstant, 185 | node.content, 186 | node.closeWrapperConstant, 187 | node.optionalConstant, 188 | ); 189 | 190 | break; 191 | 192 | case Ast.NodeKind.FieldSpecification: 193 | linearLength = await sumLinearLengths( 194 | state, 195 | trace.id, 196 | 0, 197 | node.optionalConstant, 198 | node.name, 199 | node.fieldTypeSpecification, 200 | ); 201 | 202 | break; 203 | 204 | case Ast.NodeKind.FieldSpecificationList: { 205 | const elements: ReadonlyArray> = node.content.elements; 206 | 207 | let initialLength: number = 0; 208 | 209 | if (node.openRecordMarkerConstant && elements.length) { 210 | initialLength += 2; 211 | } 212 | 213 | linearLength = await sumLinearLengths( 214 | state, 215 | trace.id, 216 | initialLength, 217 | node.openWrapperConstant, 218 | node.closeWrapperConstant, 219 | node.openRecordMarkerConstant, 220 | ...elements, 221 | ); 222 | 223 | break; 224 | } 225 | 226 | case Ast.NodeKind.FieldTypeSpecification: 227 | linearLength = await sumLinearLengths(state, trace.id, 2, node.equalConstant, node.fieldType); 228 | break; 229 | 230 | case Ast.NodeKind.FunctionExpression: { 231 | let initialLength: number = 2; 232 | 233 | if (node.functionReturnType) { 234 | initialLength += 2; 235 | } 236 | 237 | linearLength = await sumLinearLengths( 238 | state, 239 | trace.id, 240 | initialLength, 241 | node.parameters, 242 | node.functionReturnType, 243 | node.fatArrowConstant, 244 | node.expression, 245 | ); 246 | 247 | break; 248 | } 249 | 250 | case Ast.NodeKind.FunctionType: 251 | linearLength = await sumLinearLengths( 252 | state, 253 | trace.id, 254 | 2, 255 | node.functionConstant, 256 | node.parameters, 257 | node.functionReturnType, 258 | ); 259 | 260 | break; 261 | 262 | case Ast.NodeKind.GeneralizedIdentifier: 263 | case Ast.NodeKind.Identifier: 264 | linearLength = node.literal.length; 265 | break; 266 | 267 | case Ast.NodeKind.IdentifierExpression: 268 | linearLength = await sumLinearLengths(state, trace.id, 0, node.inclusiveConstant, node.identifier); 269 | break; 270 | 271 | case Ast.NodeKind.ItemAccessExpression: 272 | linearLength = await sumLinearLengths( 273 | state, 274 | trace.id, 275 | 0, 276 | node.openWrapperConstant, 277 | node.content, 278 | node.closeWrapperConstant, 279 | node.optionalConstant, 280 | ); 281 | 282 | break; 283 | 284 | case Ast.NodeKind.LiteralExpression: 285 | linearLength = node.literal.length; 286 | break; 287 | 288 | case Ast.NodeKind.ListType: 289 | linearLength = await sumLinearLengths( 290 | state, 291 | trace.id, 292 | 0, 293 | node.openWrapperConstant, 294 | node.content, 295 | node.closeWrapperConstant, 296 | ); 297 | 298 | break; 299 | 300 | case Ast.NodeKind.MetadataExpression: { 301 | linearLength = await sumLinearLengths(state, trace.id, 2, node.left, node.operatorConstant, node.right); 302 | break; 303 | } 304 | 305 | case Ast.NodeKind.NotImplementedExpression: 306 | linearLength = await sumLinearLengths(state, trace.id, 0, node.ellipsisConstant); 307 | break; 308 | 309 | case Ast.NodeKind.Parameter: { 310 | let initialLength: number = 0; 311 | 312 | if (node.optionalConstant) { 313 | initialLength += 1; 314 | } 315 | 316 | if (node.parameterType) { 317 | initialLength += 1; 318 | } 319 | 320 | linearLength = await sumLinearLengths( 321 | state, 322 | trace.id, 323 | initialLength, 324 | node.optionalConstant, 325 | node.name, 326 | node.parameterType, 327 | ); 328 | 329 | break; 330 | } 331 | 332 | case Ast.NodeKind.ParenthesizedExpression: 333 | linearLength = await sumLinearLengths( 334 | state, 335 | trace.id, 336 | 0, 337 | node.openWrapperConstant, 338 | node.content, 339 | node.closeWrapperConstant, 340 | ); 341 | 342 | break; 343 | 344 | case Ast.NodeKind.PrimitiveType: 345 | linearLength = node.primitiveTypeKind.length; 346 | break; 347 | 348 | case Ast.NodeKind.RangeExpression: 349 | linearLength = await sumLinearLengths(state, trace.id, 0, node.left, node.rangeConstant, node.right); 350 | break; 351 | 352 | case Ast.NodeKind.RecordType: 353 | linearLength = await sumLinearLengths(state, trace.id, 0, node.fields); 354 | break; 355 | 356 | case Ast.NodeKind.RecursivePrimaryExpression: 357 | linearLength = await sumLinearLengths(state, trace.id, 0, node.head, ...node.recursiveExpressions.elements); 358 | break; 359 | 360 | case Ast.NodeKind.SectionMember: { 361 | let initialLength: number = 0; 362 | 363 | if (node.literalAttributes) { 364 | initialLength += 1; 365 | } 366 | 367 | if (node.sharedConstant) { 368 | initialLength += 1; 369 | } 370 | 371 | linearLength = await sumLinearLengths( 372 | state, 373 | trace.id, 374 | initialLength, 375 | node.literalAttributes, 376 | node.sharedConstant, 377 | node.namePairedExpression, 378 | node.semicolonConstant, 379 | ); 380 | 381 | break; 382 | } 383 | 384 | case Ast.NodeKind.Section: { 385 | const sectionMembers: ReadonlyArray = node.sectionMembers.elements; 386 | 387 | let initialLength: number = 0; 388 | 389 | if (node.literalAttributes) { 390 | initialLength += 1; 391 | } 392 | 393 | if (node.name) { 394 | initialLength += 1; 395 | } 396 | 397 | linearLength = await sumLinearLengths( 398 | state, 399 | trace.id, 400 | initialLength, 401 | node.literalAttributes, 402 | node.sectionConstant, 403 | node.name, 404 | node.semicolonConstant, 405 | ...sectionMembers, 406 | ); 407 | 408 | break; 409 | } 410 | 411 | case Ast.NodeKind.TableType: 412 | linearLength = await sumLinearLengths(state, trace.id, 1, node.tableConstant, node.rowType); 413 | break; 414 | 415 | case Ast.NodeKind.UnaryExpression: 416 | linearLength = await sumLinearLengths(state, trace.id, 1, node.typeExpression, ...node.operators.elements); 417 | break; 418 | 419 | // is always multiline, therefore cannot have linear line length 420 | case Ast.NodeKind.IfExpression: 421 | linearLength = await sumLinearLengths( 422 | state, 423 | trace.id, 424 | 1, 425 | node.ifConstant, 426 | node.condition, 427 | node.thenConstant, 428 | node.trueExpression, 429 | node.elseConstant, 430 | node.falseExpression, 431 | ); 432 | 433 | break; 434 | case Ast.NodeKind.LetExpression: 435 | linearLength = await sumLinearLengths( 436 | state, 437 | trace.id, 438 | 1, 439 | node.letConstant, 440 | node.variableList, 441 | node.inConstant, 442 | node.expression, 443 | ); 444 | 445 | break; 446 | 447 | default: 448 | throw PQP.Assert.isNever(node); 449 | } 450 | 451 | state.linearLengthMap.set(node.id, linearLength); 452 | state.result = linearLength; 453 | 454 | trace.exit({ linearLength }); 455 | } 456 | 457 | // eslint-disable-next-line require-await 458 | async function visitBinOpExpressionNode( 459 | state: LinearLengthState, 460 | node: Ast.TBinOpExpression, 461 | correlationId: number | undefined, 462 | ): Promise { 463 | return sumLinearLengths( 464 | state, 465 | correlationId, 466 | node.operatorConstant.constantKind.length, 467 | node.left, 468 | node.operatorConstant, 469 | node.right, 470 | ); 471 | } 472 | 473 | function visitWrappedCsvArray( 474 | state: LinearLengthState, 475 | node: 476 | | Ast.InvokeExpression 477 | | Ast.ListExpression 478 | | Ast.ListLiteral 479 | | Ast.TParameterList 480 | | Ast.RecordExpression 481 | | Ast.RecordLiteral, 482 | correlationId: number | undefined, 483 | ): Promise { 484 | const elements: ReadonlyArray = node.content.elements; 485 | const numElements: number = elements.length; 486 | 487 | return sumLinearLengths( 488 | state, 489 | correlationId, 490 | numElements ? numElements - 1 : 0, 491 | node.openWrapperConstant, 492 | node.closeWrapperConstant, 493 | ...elements, 494 | ); 495 | } 496 | 497 | async function sumLinearLengths( 498 | state: LinearLengthState, 499 | correlationId: number | undefined, 500 | initialLength: number, 501 | ...nodes: (Ast.TNode | undefined)[] 502 | ): Promise { 503 | const trace: Trace = state.traceManager.entry( 504 | FormatTraceConstant.LinearLength, 505 | sumLinearLengths.name, 506 | correlationId, 507 | ); 508 | 509 | const filteredNodes: Ast.TNode[] = nodes.filter( 510 | (value: Ast.TNode | undefined): value is Ast.TNode => value !== undefined, 511 | ); 512 | 513 | const linearLengths: ReadonlyArray = await PQP.ArrayUtils.mapAsync(filteredNodes, (node: Ast.TNode) => 514 | getLinearLength( 515 | state.nodeIdMapCollection, 516 | state.linearLengthMap, 517 | node, 518 | state.locale, 519 | state.traceManager, 520 | trace.id, 521 | state.cancellationToken, 522 | ), 523 | ); 524 | 525 | const result: number = linearLengths.reduce( 526 | (sum: number, linearLength: number) => sum + linearLength, 527 | initialLength, 528 | ); 529 | 530 | trace.exit(); 531 | 532 | return result; 533 | } 534 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { 5 | EqualityOperator, 6 | KeywordConstant, 7 | LogicalOperator, 8 | MiscConstant, 9 | WrapperConstant, 10 | } from "@microsoft/powerquery-parser/lib/powerquery-parser/language/constant/constant"; 11 | import { NodeKind as NK } from "@microsoft/powerquery-parser/lib/powerquery-parser/language/ast/ast"; 12 | 13 | import { IRawTheme } from "./types"; 14 | import { scopeNameFromConstKd } from "./scopeNameHelpers"; 15 | import { SerializeParameter } from "../passes"; 16 | 17 | const StatementContainers: ReadonlyArray = [ 18 | NK.IfExpression, 19 | NK.EachExpression, 20 | NK.ErrorHandlingExpression, 21 | NK.ErrorRaisingExpression, 22 | NK.FunctionExpression, 23 | NK.LetExpression, 24 | NK.OtherwiseExpression, 25 | ]; 26 | 27 | const ExpressionContainers: ReadonlyArray = [ 28 | NK.ParenthesizedExpression, 29 | NK.ArrayWrapper, 30 | NK.ArithmeticExpression, 31 | NK.AsExpression, 32 | NK.MetadataExpression, 33 | NK.ParameterList, 34 | NK.IdentifierExpression, 35 | NK.EqualityExpression, 36 | NK.LogicalExpression, 37 | NK.IdentifierPairedExpression, 38 | NK.GeneralizedIdentifierPairedExpression, 39 | NK.FieldSpecificationList, 40 | NK.FieldSpecification, 41 | NK.RecordExpression, 42 | NK.ListExpression, 43 | NK.RecordLiteral, 44 | NK.FieldSelector, 45 | NK.FieldProjection, 46 | ]; 47 | 48 | export const ContainerSet: ReadonlySet = new Set([...StatementContainers, ...ExpressionContainers]); 49 | 50 | export const defaultTheme: IRawTheme = { 51 | name: "default", 52 | settings: [ 53 | // common 54 | { 55 | scope: StatementContainers, 56 | parameters: { 57 | container: true, 58 | }, 59 | }, 60 | { 61 | scope: ExpressionContainers, 62 | parameters: { 63 | container: true, 64 | skipPostContainerNewLine: true, 65 | }, 66 | }, 67 | { 68 | scope: [ 69 | `${NK.RecordExpression}> ${NK.ArrayWrapper}`, 70 | `${NK.RecordLiteral}> ${NK.ArrayWrapper}`, 71 | `${NK.ListExpression}> ${NK.ArrayWrapper}`, 72 | `${NK.Csv}> ${NK.RecordExpression}`, 73 | `${NK.Csv}> ${NK.RecordLiteral}`, 74 | ], 75 | parameters: { 76 | container: true, 77 | skipPostContainerNewLine: true, 78 | inheritParentMode: true, 79 | }, 80 | }, 81 | { 82 | scope: [`${NK.ArrayWrapper}> ${NK.Csv}`], 83 | parameters: { 84 | inheritParentMode: true, 85 | }, 86 | }, 87 | { 88 | scope: ["constant", NK.LiteralExpression, NK.PrimitiveType, NK.GeneralizedIdentifier, NK.Identifier], 89 | parameters: { 90 | leftPadding: true, 91 | rightPadding: true, 92 | }, 93 | }, 94 | { 95 | scope: [ 96 | "constant.arithmetic-operator", 97 | `${scopeNameFromConstKd(KeywordConstant.As)}`, 98 | `${scopeNameFromConstKd(KeywordConstant.Meta)}`, 99 | ], 100 | parameters: { 101 | leftPadding: true, 102 | rightPadding: true, 103 | }, 104 | }, 105 | // list & record blocks 106 | { 107 | scope: `${scopeNameFromConstKd(MiscConstant.Comma)}`, 108 | parameters: { 109 | rightPadding: true, 110 | contentDivider: "R", 111 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 112 | }, 113 | }, 114 | { 115 | scope: [ 116 | `${scopeNameFromConstKd(WrapperConstant.LeftBrace)}`, 117 | `${scopeNameFromConstKd(WrapperConstant.LeftBracket)}`, 118 | `${scopeNameFromConstKd(WrapperConstant.LeftParenthesis)}`, 119 | ], 120 | parameters: { 121 | leftPadding: true, 122 | blockOpener: "R", 123 | noWhitespaceAppended: true, 124 | }, 125 | }, 126 | { 127 | scope: [ 128 | `${scopeNameFromConstKd(WrapperConstant.RightBrace)}`, 129 | `${scopeNameFromConstKd(WrapperConstant.RightBracket)}`, 130 | `${scopeNameFromConstKd(WrapperConstant.RightParenthesis)}`, 131 | ], 132 | parameters: { 133 | rightPadding: true, 134 | blockCloser: "L", 135 | noWhiteSpaceBetweenWhenNoContentBetweenOpenerAndCloser: true, 136 | clearTailingWhitespaceBeforeAppending: true, 137 | }, 138 | }, 139 | // each expressions 140 | { 141 | scope: [`${scopeNameFromConstKd(KeywordConstant.Each)}`], 142 | parameters: { 143 | blockOpener: "R", 144 | }, 145 | }, 146 | // if then else blocks 147 | { 148 | scope: [NK.IfExpression], 149 | parameters: { 150 | container: true, 151 | dedentContainerConditionReg: /(else)[\s]*$/g, 152 | ignoreInline: true, 153 | }, 154 | }, 155 | { 156 | scope: [`${NK.IfExpression}> ${NK.EqualityExpression}`, `${NK.IfExpression}> ${NK.LogicalExpression}`], 157 | parameters: { 158 | container: true, 159 | skipPostContainerNewLine: false, 160 | contentDivider: "R", 161 | }, 162 | }, 163 | { 164 | scope: [`${scopeNameFromConstKd(KeywordConstant.Then)}`], 165 | parameters: { 166 | leftPadding: true, 167 | blockOpener: "R", 168 | }, 169 | }, 170 | { 171 | scope: [`${scopeNameFromConstKd(KeywordConstant.Else)}`], 172 | parameters: { 173 | blockCloser: "L", 174 | blockOpener: "R", 175 | }, 176 | }, 177 | // try otherwise error 178 | { 179 | scope: [`${scopeNameFromConstKd(KeywordConstant.Try)}`], 180 | parameters: { 181 | leftPadding: true, 182 | blockOpener: "R", 183 | }, 184 | }, 185 | { 186 | scope: [`${scopeNameFromConstKd(KeywordConstant.Otherwise)}`], 187 | parameters: { 188 | blockCloser: "L", 189 | blockOpener: "R", 190 | }, 191 | }, 192 | { 193 | scope: [`${scopeNameFromConstKd(KeywordConstant.Error)}`], 194 | parameters: { 195 | leftPadding: true, 196 | blockOpener: "R", 197 | }, 198 | }, 199 | // function expression 200 | { 201 | scope: [`${NK.ParameterList}> ${scopeNameFromConstKd(WrapperConstant.RightParenthesis)}`], 202 | parameters: { 203 | blockCloser: "L", 204 | noWhiteSpaceBetweenWhenNoContentBetweenOpenerAndCloser: true, 205 | clearTailingWhitespaceBeforeAppending: true, 206 | }, 207 | }, 208 | { 209 | scope: [`${scopeNameFromConstKd(MiscConstant.FatArrow)}`], 210 | parameters: { 211 | blockOpener: "R", 212 | }, 213 | }, 214 | // ItemAccessExpression 215 | { 216 | scope: [`${NK.ItemAccessExpression}> ${scopeNameFromConstKd(WrapperConstant.LeftBrace)}`], 217 | parameters: { 218 | leftPadding: true, 219 | blockOpener: "R", 220 | noWhitespaceAppended: true, 221 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 222 | }, 223 | }, 224 | // InvokeExpression 225 | { 226 | scope: [`${NK.InvokeExpression}> ${scopeNameFromConstKd(WrapperConstant.LeftParenthesis)}`], 227 | parameters: { 228 | leftPadding: true, 229 | blockOpener: "R", 230 | noWhitespaceAppended: true, 231 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 232 | }, 233 | }, 234 | // LetExpression 235 | { 236 | scope: [NK.LetExpression], 237 | parameters: { 238 | container: true, 239 | ignoreInline: true, 240 | }, 241 | }, 242 | { 243 | scope: [`${scopeNameFromConstKd(KeywordConstant.Let)}`], 244 | parameters: { 245 | leftPadding: true, 246 | blockOpener: "R", 247 | }, 248 | }, 249 | { 250 | scope: [`${scopeNameFromConstKd(KeywordConstant.In)}`], 251 | parameters: { 252 | leftPadding: true, 253 | blockCloser: "L", 254 | blockOpener: "R", 255 | }, 256 | }, 257 | // RangeExpression 258 | { 259 | scope: [ 260 | `${NK.RangeExpression}> ${NK.LiteralExpression}`, 261 | `${NK.RangeExpression}> ${NK.GeneralizedIdentifier}`, 262 | `${NK.RangeExpression}> ${NK.Identifier}`, 263 | ], 264 | parameters: { 265 | leftPadding: false, 266 | rightPadding: false, 267 | }, 268 | }, 269 | { 270 | scope: [`${NK.RangeExpression}> ${scopeNameFromConstKd(MiscConstant.DotDot)}`], 271 | parameters: { 272 | leftPadding: false, 273 | rightPadding: false, 274 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 275 | }, 276 | }, 277 | // UnaryExpression 278 | { 279 | scope: [ 280 | `${NK.UnaryExpression}> ${NK.ArrayWrapper}> constant.arithmetic-operator`, 281 | `${NK.UnaryExpression}> ${NK.LiteralExpression}`, 282 | ], 283 | parameters: { 284 | contentDivider: undefined, 285 | leftPadding: false, 286 | rightPadding: false, 287 | }, 288 | }, 289 | // IdentifierExpression 290 | { 291 | scope: [ 292 | `${NK.IdentifierExpression}> ${scopeNameFromConstKd(MiscConstant.AtSign)}`, 293 | `${NK.IdentifierExpression}> ${NK.Identifier}`, 294 | ], 295 | parameters: { 296 | contentDivider: undefined, 297 | leftPadding: false, 298 | rightPadding: false, 299 | }, 300 | }, 301 | // Session 302 | { 303 | scope: `${scopeNameFromConstKd(MiscConstant.Semicolon)}`, 304 | parameters: { 305 | lineBreak: "R", 306 | rightPadding: true, 307 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 308 | }, 309 | }, 310 | { 311 | scope: `${NK.Section}> ${scopeNameFromConstKd(MiscConstant.Semicolon)}`, 312 | parameters: { 313 | doubleLineBreak: "R", 314 | rightPadding: true, 315 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 316 | }, 317 | }, 318 | { 319 | scope: `${NK.Section}> ${NK.ArrayWrapper}> ${NK.SectionMember}> ${scopeNameFromConstKd( 320 | MiscConstant.Semicolon, 321 | )}`, 322 | parameters: { 323 | lineBreak: "R", 324 | rightPadding: true, 325 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 326 | }, 327 | }, 328 | { 329 | scope: [`${NK.Section}> ${NK.RecordLiteral}> ${scopeNameFromConstKd(WrapperConstant.RightBracket)}`], 330 | parameters: { 331 | rightPadding: true, 332 | blockCloser: "L", 333 | lineBreak: "R", 334 | noWhiteSpaceBetweenWhenNoContentBetweenOpenerAndCloser: true, 335 | clearTailingWhitespaceBeforeAppending: true, 336 | }, 337 | }, 338 | { 339 | scope: [`${NK.SectionMember}> ${scopeNameFromConstKd(KeywordConstant.Shared)}`], 340 | parameters: { 341 | lineBreak: "L", 342 | }, 343 | }, 344 | // IdentifierPairedExpression 345 | { 346 | scope: [`${NK.IdentifierPairedExpression}> ${scopeNameFromConstKd(EqualityOperator.EqualTo)}`], 347 | parameters: { 348 | rightPadding: true, 349 | blockOpener: "R", 350 | blockOpenerActivatedMatcher: /^[\s]*(if|let|try)/g, 351 | }, 352 | }, 353 | // FieldSelector & FieldProjection 354 | { 355 | scope: [ 356 | `${NK.RecursivePrimaryExpression}> ${NK.FieldSelector}> ${scopeNameFromConstKd( 357 | WrapperConstant.LeftBracket, 358 | )}`, 359 | `${NK.ArrayWrapper}> ${NK.FieldSelector}> ${scopeNameFromConstKd(WrapperConstant.LeftBracket)}`, 360 | `${NK.ArrayWrapper}> ${NK.FieldProjection}> ${scopeNameFromConstKd(WrapperConstant.LeftBracket)}`, 361 | ], 362 | parameters: { 363 | blockOpener: "R", 364 | noWhitespaceAppended: true, 365 | clearTailingWhitespaceBeforeAppending: true, 366 | }, 367 | }, 368 | { 369 | scope: [ 370 | `${NK.FieldSelector}> ${scopeNameFromConstKd(MiscConstant.QuestionMark)}`, 371 | `${NK.FieldProjection}> ${scopeNameFromConstKd(MiscConstant.QuestionMark)}`, 372 | ], 373 | parameters: { 374 | clearTailingWhitespaceCarriageReturnBeforeAppending: true, 375 | }, 376 | }, 377 | // LogicalExpression & EqualityExpression & ArithmeticExpression 378 | { 379 | scope: [`${scopeNameFromConstKd(LogicalOperator.And)}`, `${scopeNameFromConstKd(LogicalOperator.Or)}`], 380 | parameters: { 381 | contentDivider: "L", 382 | leftPadding: true, 383 | rightPadding: true, 384 | }, 385 | }, 386 | { 387 | scope: [`${scopeNameFromConstKd(MiscConstant.Ampersand)}`], 388 | parameters: { 389 | blockOpener: "L", 390 | leftPadding: true, 391 | rightPadding: true, 392 | }, 393 | }, 394 | { 395 | scope: [ 396 | `${NK.LogicalExpression}> ${NK.LogicalExpression}`, 397 | `${NK.EqualityExpression}> ${NK.LogicalExpression}`, 398 | `${NK.EqualityExpression}> ${NK.EqualityExpression}`, 399 | `${NK.LogicalExpression}> ${NK.EqualityExpression}`, 400 | `${NK.ArithmeticExpression}> ${NK.ArithmeticExpression}`, 401 | ], 402 | parameters: { 403 | container: true, 404 | skipPostContainerNewLine: true, 405 | ignoreInline: true, 406 | }, 407 | }, 408 | { 409 | scope: [ 410 | `${NK.IdentifierPairedExpression}> ${NK.LogicalExpression}`, 411 | `${NK.IdentifierPairedExpression}> ${NK.EqualityExpression}`, 412 | `${NK.IfExpression}> ${NK.LogicalExpression}`, 413 | `${NK.IfExpression}> ${NK.EqualityExpression}`, 414 | ], 415 | parameters: { 416 | container: true, 417 | skipPostContainerNewLine: true, 418 | blockOpener: "L", 419 | }, 420 | }, 421 | ], 422 | }; 423 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export * from "./types"; 5 | export { ContainerSet } from "./constants"; 6 | export { ThemeTrieElementRule } from "./themes"; 7 | export { SyncThemeRegistry } from "./register"; 8 | export { ScopeMetadata, ScopeMetadataProvider, ScopeListElement, StackElement } from "./scopes"; 9 | export { getNodeScopeName } from "./scopeNameHelpers"; 10 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/register.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { IRawTheme, IThemeProvider, RegistryOptions } from "./types"; 5 | import { Theme, ThemeTrieElementRule } from "./themes"; 6 | import { defaultTheme } from "./constants"; 7 | import { ScopeMetadataProvider } from "./scopes"; 8 | import { SerializeParameter } from "../passes"; 9 | 10 | const DEFAULT_OPTIONS: RegistryOptions = { 11 | theme: defaultTheme, 12 | }; 13 | 14 | export class SyncThemeRegistry implements IThemeProvider { 15 | static defaultInstance: SyncThemeRegistry = new SyncThemeRegistry(); 16 | 17 | private _theme: Theme; 18 | public readonly scopeMetaProvider: ScopeMetadataProvider; 19 | constructor(private readonly _option: RegistryOptions = DEFAULT_OPTIONS) { 20 | this._theme = Theme.createFromRawTheme(this._option.theme); 21 | this.scopeMetaProvider = new ScopeMetadataProvider(this); 22 | } 23 | 24 | /** 25 | * Update the theme 26 | * @param theme new theme 27 | */ 28 | public setTheme(theme: IRawTheme): void { 29 | this._theme = Theme.createFromRawTheme(theme); 30 | this.scopeMetaProvider.onDidChangeTheme(); 31 | } 32 | 33 | /** 34 | * Get the default theme settings 35 | */ 36 | public getDefaults(): ThemeTrieElementRule { 37 | return this._theme.getDefaults(); 38 | } 39 | 40 | /** 41 | * Match a scope in the theme. 42 | */ 43 | public themeMatch(scopeName: string): ThemeTrieElementRule[] { 44 | return this._theme.match(scopeName); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/scopeNameHelpers.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | import { 6 | ArithmeticOperator, 7 | EqualityOperator, 8 | KeywordConstant, 9 | LanguageConstant, 10 | LogicalOperator, 11 | MiscConstant, 12 | PrimitiveTypeConstant, 13 | RelationalOperator, 14 | TConstant, 15 | UnaryOperator, 16 | WrapperConstant, 17 | } from "@microsoft/powerquery-parser/lib/powerquery-parser/language/constant/constant"; 18 | import { Ast } from "@microsoft/powerquery-parser/lib/powerquery-parser/language"; 19 | 20 | export function scopeNameFromConstKd(constantKind: TConstant): string { 21 | switch (constantKind) { 22 | case ArithmeticOperator.Multiplication: 23 | return "constant.arithmetic-operator.multiplication"; 24 | case ArithmeticOperator.Division: 25 | return "constant.arithmetic-operator.division"; 26 | case ArithmeticOperator.Addition: 27 | return "constant.arithmetic-operator.addition"; 28 | case ArithmeticOperator.Subtraction: 29 | return "constant.arithmetic-operator.subtraction"; 30 | case ArithmeticOperator.And: 31 | return "constant.arithmetic-operator.and"; 32 | case EqualityOperator.EqualTo: 33 | return "constant.equality-operator.equal-to"; 34 | case EqualityOperator.NotEqualTo: 35 | return "constant.equality-operator.not-equal-to"; 36 | case KeywordConstant.As: 37 | return "constant.keyword.as"; 38 | case KeywordConstant.Each: 39 | return "constant.keyword.each"; 40 | case KeywordConstant.Else: 41 | return "constant.keyword.else"; 42 | case KeywordConstant.Error: 43 | return "constant.keyword.error"; 44 | case KeywordConstant.False: 45 | return "constant.keyword.false"; 46 | case KeywordConstant.If: 47 | return "constant.keyword.if"; 48 | case KeywordConstant.In: 49 | return "constant.keyword.in"; 50 | case KeywordConstant.Is: 51 | return "constant.keyword.is"; 52 | case KeywordConstant.Let: 53 | return "constant.keyword.let"; 54 | case KeywordConstant.Meta: 55 | return "constant.keyword.meta"; 56 | case KeywordConstant.Otherwise: 57 | return "constant.keyword.otherwise"; 58 | case KeywordConstant.Section: 59 | return "constant.keyword.section"; 60 | case KeywordConstant.Shared: 61 | return "constant.keyword.shared"; 62 | case KeywordConstant.Then: 63 | return "constant.keyword.then"; 64 | case KeywordConstant.True: 65 | return "constant.keyword.true"; 66 | case KeywordConstant.Try: 67 | return "constant.keyword.try"; 68 | case KeywordConstant.Type: 69 | return "constant.keyword.type"; 70 | case LanguageConstant.Catch: 71 | return "constant.language.catch"; 72 | case LanguageConstant.Nullable: 73 | return "constant.language.nullable"; 74 | case LanguageConstant.Optional: 75 | return "constant.language.optional"; 76 | case LogicalOperator.And: 77 | return "constant.language.and"; 78 | case LogicalOperator.Or: 79 | return "constant.language.or"; 80 | case MiscConstant.Ampersand: 81 | return "constant.misc.ampersand"; 82 | case MiscConstant.AtSign: 83 | return "constant.misc.at-sign"; 84 | case MiscConstant.Comma: 85 | return "constant.misc.coma"; 86 | case MiscConstant.DotDot: 87 | return "constant.misc.dot-dot"; 88 | case MiscConstant.Ellipsis: 89 | return "constant.misc.ellipsis"; 90 | case MiscConstant.Equal: 91 | return "constant.misc.equal"; 92 | case MiscConstant.FatArrow: 93 | return "constant.misc.fat-arrow"; 94 | case MiscConstant.NullCoalescingOperator: 95 | return "constant.misc.null-coalescing-operator"; 96 | case MiscConstant.Semicolon: 97 | return "constant.misc.semicolon"; 98 | case MiscConstant.QuestionMark: 99 | return "constant.misc.question-mark"; 100 | case PrimitiveTypeConstant.Action: 101 | return "constant.primitive-type.action"; 102 | case PrimitiveTypeConstant.Any: 103 | return "constant.primitive-type.any"; 104 | case PrimitiveTypeConstant.AnyNonNull: 105 | return "constant.primitive-type.any-not-null"; 106 | case PrimitiveTypeConstant.Binary: 107 | return "constant.primitive-type.binary"; 108 | case PrimitiveTypeConstant.Date: 109 | return "constant.primitive-type.date"; 110 | case PrimitiveTypeConstant.DateTime: 111 | return "constant.primitive-type.date-time"; 112 | case PrimitiveTypeConstant.DateTimeZone: 113 | return "constant.primitive-type.date-time-zone"; 114 | case PrimitiveTypeConstant.Duration: 115 | return "constant.primitive-type.duration"; 116 | case PrimitiveTypeConstant.Function: 117 | return "constant.primitive-type.function"; 118 | case PrimitiveTypeConstant.List: 119 | return "constant.primitive-type.list"; 120 | case PrimitiveTypeConstant.Logical: 121 | return "constant.primitive-type.logical"; 122 | case PrimitiveTypeConstant.None: 123 | return "constant.primitive-type.none"; 124 | case PrimitiveTypeConstant.Null: 125 | return "constant.primitive-type.null"; 126 | case PrimitiveTypeConstant.Number: 127 | return "constant.primitive-type.number"; 128 | case PrimitiveTypeConstant.Record: 129 | return "constant.primitive-type.record"; 130 | case PrimitiveTypeConstant.Table: 131 | return "constant.primitive-type.table"; 132 | case PrimitiveTypeConstant.Text: 133 | return "constant.primitive-type.text"; 134 | case PrimitiveTypeConstant.Time: 135 | return "constant.primitive-type.time"; 136 | case PrimitiveTypeConstant.Type: 137 | return "constant.primitive-type.type"; 138 | case RelationalOperator.LessThan: 139 | return "constant.relational-operator.less-than"; 140 | case RelationalOperator.LessThanEqualTo: 141 | return "constant.relational-operator.less-than-equal-to"; 142 | case RelationalOperator.GreaterThan: 143 | return "constant.relational-operator.greater-than"; 144 | case RelationalOperator.GreaterThanEqualTo: 145 | return "constant.relational-operator.greater-than-equal-to"; 146 | case UnaryOperator.Positive: 147 | return "constant.unary-operator.positive"; 148 | case UnaryOperator.Negative: 149 | return "constant.unary-operator.negative"; 150 | case UnaryOperator.Not: 151 | return "constant.unary-operator.not"; 152 | case WrapperConstant.LeftBrace: 153 | return "constant.wrapper.left-brace"; 154 | case WrapperConstant.LeftBracket: 155 | return "constant.wrapper.left-bracket"; 156 | case WrapperConstant.LeftParenthesis: 157 | return "constant.wrapper.left-parenthesis"; 158 | case WrapperConstant.RightBrace: 159 | return "constant.wrapper.right-parenthesis"; 160 | case WrapperConstant.RightBracket: 161 | return "constant.wrapper.right-bracket"; 162 | case WrapperConstant.RightParenthesis: 163 | return "constant.wrapper.right-parenthesis"; 164 | 165 | default: { 166 | // comment one of the cases above to see a ts compile time never covariance error 167 | throw PQP.Assert.isNever(constantKind); 168 | } 169 | } 170 | } 171 | 172 | export function getNodeScopeName(node: Ast.TNode): string { 173 | return node.kind === Ast.NodeKind.Constant ? scopeNameFromConstKd(node.constantKind) : node.kind; 174 | } 175 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/scopes.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | 6 | import { IParameters, IThemeProvider } from "./types"; 7 | import { ThemeTrieElementRule } from "./themes"; 8 | 9 | /** 10 | * The metadata containing the scopeName and matched theme rules of a scope name 11 | */ 12 | export class ScopeMetadata { 13 | constructor(public readonly scopeName: string, public readonly themeData: ThemeTrieElementRule[]) {} 14 | } 15 | 16 | export class ScopeMetadataProvider { 17 | private static _NULL_SCOPE_METADATA: ScopeMetadata = new ScopeMetadata("", []); 18 | 19 | private _cache: Map> = new Map>(); 20 | private _defaultMetaData: ScopeMetadata = new ScopeMetadata("", []); 21 | 22 | private _doGetMetadataForScope(scopeName: string): ScopeMetadata { 23 | const themeData: ThemeTrieElementRule[] = this._themeProvider.themeMatch(scopeName); 24 | 25 | return new ScopeMetadata(scopeName, themeData); 26 | } 27 | public getMetadataForScope(scopeName: string | undefined): ScopeMetadata { 28 | // never hurts to be too careful 29 | if (scopeName === null || scopeName === undefined) { 30 | return ScopeMetadataProvider._NULL_SCOPE_METADATA as ScopeMetadata; 31 | } 32 | 33 | let value: ScopeMetadata | undefined = this._cache.get(scopeName); 34 | 35 | if (value) { 36 | return value; 37 | } 38 | 39 | value = this._doGetMetadataForScope(scopeName); 40 | this._cache.set(scopeName, value); 41 | 42 | return value; 43 | } 44 | 45 | constructor(private readonly _themeProvider: IThemeProvider) { 46 | this.onDidChangeTheme(); 47 | } 48 | public onDidChangeTheme(): void { 49 | this._cache = new Map(); 50 | this._defaultMetaData = new ScopeMetadata("", [this._themeProvider.getDefaults()]); 51 | } 52 | 53 | public getDefaultMetadata(): ScopeMetadata { 54 | return this._defaultMetaData; 55 | } 56 | } 57 | 58 | export const EmptyScopeListElementParameters: IParameters = {}; 59 | 60 | /** 61 | * Immutable scope list element 62 | */ 63 | export class ScopeListElement { 64 | constructor( 65 | public readonly parent: ScopeListElement | undefined, 66 | public readonly scope: string, 67 | public readonly parameters: T, 68 | ) {} 69 | 70 | private static _equals( 71 | l: ScopeListElement, 72 | r: ScopeListElement, 73 | ): boolean { 74 | let a: ScopeListElement | undefined = l; 75 | let b: ScopeListElement | undefined = r; 76 | 77 | do { 78 | if (a === b) { 79 | return true; 80 | } 81 | 82 | if (a.scope !== b.scope) { 83 | return false; 84 | } 85 | 86 | // Go to previous pair 87 | a = a.parent; 88 | b = b.parent; 89 | 90 | // unsafe a, b might be null 91 | 92 | if (!a && !b) { 93 | // End of list reached for both 94 | return true; 95 | } 96 | 97 | if (!a || !b) { 98 | // End of list reached only for one 99 | return false; 100 | } 101 | // safe a, b cannot be null 102 | // eslint-disable-next-line no-constant-condition 103 | } while (true); 104 | } 105 | 106 | public equals(other: ScopeListElement): boolean { 107 | return ScopeListElement._equals(this, other); 108 | } 109 | 110 | private static _hasScopeNameCombinator(scopeName: string): boolean { 111 | if (scopeName.length < 1) { 112 | return false; 113 | } 114 | 115 | return scopeName[scopeName.length - 1] === ">"; 116 | } 117 | 118 | private static _purgeScopeNameCombinator(scopeName: string): [boolean, string] { 119 | if (this._hasScopeNameCombinator(scopeName)) { 120 | return [true, scopeName.substring(0, scopeName.length - 1)]; 121 | } 122 | 123 | return [false, scopeName]; 124 | } 125 | 126 | private static _matchesScope(scope: string, selector: string, selectorWithDot: string): boolean { 127 | return selector === scope || scope.substring(0, selectorWithDot.length) === selectorWithDot; 128 | } 129 | 130 | private static _matches( 131 | target: ScopeListElement | undefined, 132 | parentScopes: string[] | undefined, 133 | ): boolean { 134 | if (!parentScopes) { 135 | return true; 136 | } 137 | 138 | const len: number = parentScopes.length; 139 | let index: number = 0; 140 | 141 | // combinator would only exist in the parent scopes, but parentScope starts from the second 142 | const [isFirstScopeNameCombinator, firstScopeName]: [boolean, string] = this._purgeScopeNameCombinator( 143 | parentScopes[index], 144 | ); 145 | 146 | let selector: string = firstScopeName; 147 | let selectorWithDot: string = `${selector}.`; 148 | let hasCombinator: boolean = isFirstScopeNameCombinator; 149 | 150 | while (target) { 151 | if (this._matchesScope(target.scope, selector, selectorWithDot)) { 152 | index = index + 1; 153 | 154 | if (index === len) { 155 | return true; 156 | } 157 | 158 | const [isNextCombinator, nextScopeName]: [boolean, string] = this._purgeScopeNameCombinator( 159 | parentScopes[index], 160 | ); 161 | 162 | hasCombinator = isNextCombinator; 163 | selector = nextScopeName; 164 | selectorWithDot = `${selector}.`; 165 | } else if (hasCombinator) { 166 | // found a mismatched scope name of combinator 167 | return false; 168 | } 169 | 170 | target = target.parent; 171 | } 172 | 173 | return false; 174 | } 175 | 176 | /** 177 | * Merge any matching rules' metadata into current target metadata 178 | * 179 | * @param parameters current target metadata record 180 | * @param scopesList current scope list element 181 | * @param source the source ScopeMetadata holding the rule might be matched 182 | * @return mergedMetaData the number of merged metadata 183 | */ 184 | public static mergeParameters( 185 | parameters: T, 186 | scopesList: ScopeListElement | undefined, 187 | source: ScopeMetadata | undefined, 188 | ): T { 189 | if (!source) { 190 | return parameters; 191 | } 192 | 193 | let assignedParameters: T | undefined = undefined; 194 | 195 | if (source.themeData) { 196 | // Find the first themeData that matches 197 | for (let i: number = 0; i < source.themeData.length; i += 1) { 198 | const themeData: ThemeTrieElementRule = source.themeData[i]; 199 | 200 | if (this._matches(scopesList, themeData.parentScopes)) { 201 | assignedParameters = themeData.parameters; 202 | break; 203 | } 204 | } 205 | } 206 | 207 | return assignedParameters ?? (EmptyScopeListElementParameters as T); 208 | } 209 | private static _push( 210 | target: ScopeListElement, 211 | scopeMetadataProvider: ScopeMetadataProvider, 212 | scopes: string[], 213 | ): ScopeListElement { 214 | for (let i: number = 0; i < scopes.length; i += 1) { 215 | const scope: string = scopes[i]; 216 | const rawMetadata: ScopeMetadata = scopeMetadataProvider.getMetadataForScope(scope); 217 | 218 | const parameters: T = ScopeListElement.mergeParameters(target.parameters, target, rawMetadata); 219 | 220 | target = new ScopeListElement(target, scope, parameters); 221 | } 222 | 223 | return target; 224 | } 225 | 226 | /** 227 | * Append scope/scopes to the current list 228 | * 229 | * @param scopeMetadataProvider 230 | * @param scope a single scopeName or multiple scopeName seperated by space 231 | */ 232 | public push(scopeMetadataProvider: ScopeMetadataProvider, scope: string | undefined): ScopeListElement { 233 | if (scope === null || scope === undefined) { 234 | // cannot push empty, return self 235 | return this; 236 | } 237 | 238 | if (scope.indexOf(" ") >= 0) { 239 | // there are multiple scopes to push 240 | return ScopeListElement._push(this, scopeMetadataProvider, scope.split(/ /g)); 241 | } 242 | 243 | // there is a single scope to push 244 | return ScopeListElement._push(this, scopeMetadataProvider, [scope]); 245 | } 246 | 247 | private static _generateScopes( 248 | scopesList: ScopeListElement | undefined, 249 | ): string[] { 250 | const result: string[] = []; 251 | let resultLen: number = 0; 252 | 253 | while (scopesList) { 254 | result[resultLen] = scopesList.scope; 255 | scopesList = scopesList.parent; 256 | resultLen += 1; 257 | } 258 | 259 | result.reverse(); 260 | 261 | return result; 262 | } 263 | 264 | /** 265 | * Generate scopes of current list descending like: 266 | * segment1.segment2.segment3 267 | * segment1.segment2 268 | * segment1 269 | */ 270 | public generateScopes(): string[] { 271 | return ScopeListElement._generateScopes(this); 272 | } 273 | } 274 | 275 | export class StackElement { 276 | /** 277 | * Ad-hoc singleton NULL stack element 278 | */ 279 | public static NULL: StackElement = new StackElement( 280 | undefined, 281 | { id: -1 } as PQP.Language.Ast.TNode, 282 | undefined as unknown as ScopeListElement, 283 | ); 284 | /** 285 | * The previous state on the stack (or null for the root state). 286 | */ 287 | public readonly parent?: StackElement; 288 | /** 289 | * The depth of the stack. 290 | */ 291 | public readonly depth: number; 292 | /** 293 | * pq ast nodes 294 | */ 295 | public readonly nodeId: number; 296 | /** 297 | * The list of scopes containing the "nodeType" for this state. 298 | */ 299 | public readonly scopeList: ScopeListElement; 300 | 301 | constructor(parent: StackElement | undefined, node: PQP.Language.Ast.TNode, scopeList: ScopeListElement) { 302 | this.parent = parent; 303 | this.depth = this.parent ? this.parent.depth + 1 : 1; 304 | this.nodeId = node.id; 305 | this.scopeList = scopeList; 306 | } 307 | 308 | /** 309 | * A structural equals check. Does not take into account `scopes`. 310 | */ 311 | private static _structuralEquals( 312 | l: StackElement, 313 | r: StackElement, 314 | ): boolean { 315 | let a: StackElement | undefined = l; 316 | let b: StackElement | undefined = r; 317 | 318 | do { 319 | if (a === b) { 320 | return true; 321 | } 322 | 323 | if (a.depth !== b.depth || a.nodeId !== b.nodeId) { 324 | return false; 325 | } 326 | 327 | // Go to previous pair 328 | a = a.parent; 329 | b = b.parent; 330 | 331 | // unsafe a, b might be null 332 | if (!a && !b) { 333 | // End of list reached for both 334 | return true; 335 | } 336 | 337 | if (!a || !b) { 338 | // End of list reached only for one 339 | return false; 340 | } 341 | // safe a, b cannot be null 342 | // eslint-disable-next-line no-constant-condition 343 | } while (true); 344 | } 345 | 346 | private static _equals(a: StackElement, b: StackElement): boolean { 347 | if (a === b) { 348 | return true; 349 | } 350 | 351 | return this._structuralEquals(a, b); 352 | } 353 | 354 | public clone(): StackElement { 355 | return this; 356 | } 357 | 358 | public equals(other: StackElement): boolean { 359 | if (!other) { 360 | return false; 361 | } 362 | 363 | return StackElement._equals(this, other); 364 | } 365 | 366 | public pop(): StackElement { 367 | return PQP.Assert.asDefined(this.parent); 368 | } 369 | 370 | public safePop(): StackElement { 371 | if (this.parent) { 372 | return this.parent; 373 | } 374 | 375 | return this; 376 | } 377 | 378 | public push(node: PQP.Language.Ast.TNode, scopeList?: ScopeListElement): StackElement { 379 | scopeList = scopeList ?? this.scopeList; 380 | 381 | return new StackElement(this, node, scopeList); 382 | } 383 | 384 | public hasSameNodeAs(other: PQP.Language.Ast.TNode): boolean { 385 | return this.nodeId === other.id; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/themes.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as PQP from "@microsoft/powerquery-parser"; 5 | 6 | import { IParameters, IRawTheme, IRawThemeSetting } from "./types"; 7 | import { strArrCmp, strcmp } from "./utils"; 8 | 9 | /** 10 | * An initially parsed rule of a scopeList from raw theme 11 | * a scope list would be like: 12 | * "scope1 scope2 scope3 scope4" 13 | */ 14 | export class ParsedThemeRule { 15 | constructor( 16 | /** 17 | * scope4 of a scope list: "scope1 scope2 scope3 scope4" 18 | */ 19 | public readonly scope: string, 20 | /** 21 | * ["scope3", "scope2", "scope1"] of a scope list: "scope1 scope2 scope3 scope4" 22 | */ 23 | public readonly parentScopes: string[] | undefined, 24 | public readonly index: number, 25 | /** 26 | * parameters of the current parsed theme rule 27 | */ 28 | public readonly parameters?: T, 29 | ) {} 30 | } 31 | 32 | /** 33 | * Parse a raw theme into first-stage ParsedThemeRule. 34 | */ 35 | export function parseTheme(source?: IRawTheme): ParsedThemeRule[] { 36 | if (!source) { 37 | return []; 38 | } 39 | 40 | if (!source.settings || !Array.isArray(source.settings)) { 41 | return []; 42 | } 43 | 44 | const settings: IRawThemeSetting[] = source.settings; 45 | const result: ParsedThemeRule[] = []; 46 | let resultLen: number = 0; 47 | 48 | for (let i: number = 0; i < settings.length; i += 1) { 49 | const entry: IRawThemeSetting = settings[i]; 50 | 51 | if (!entry.parameters) { 52 | continue; 53 | } 54 | 55 | let scopes: string[]; 56 | 57 | if (typeof entry.scope === "string") { 58 | let _scope: string = entry.scope; 59 | 60 | // remove leading commas 61 | _scope = _scope.replace(/^[,]+/, ""); 62 | 63 | // remove trailing commans 64 | _scope = _scope.replace(/[,]+$/, ""); 65 | 66 | scopes = _scope.split(","); 67 | } else if (Array.isArray(entry.scope)) { 68 | scopes = entry.scope; 69 | } else { 70 | scopes = [""]; 71 | } 72 | 73 | let parameters: T = {} as T; 74 | 75 | if (Boolean(entry.parameters) && typeof entry.parameters === "object") { 76 | parameters = entry.parameters; 77 | } 78 | 79 | for (let j: number = 0; j < scopes.length; j += 1) { 80 | const _scope: string = scopes[j].trim(); 81 | 82 | const segments: string[] = _scope.split(" "); 83 | 84 | const scope: string = segments[segments.length - 1]; 85 | let parentScopes: string[] | undefined = undefined; 86 | 87 | if (segments.length > 1) { 88 | parentScopes = segments.slice(0, segments.length - 1); 89 | parentScopes.reverse(); 90 | } 91 | 92 | result[resultLen] = new ParsedThemeRule(scope, parentScopes, i, parameters); 93 | resultLen += 1; 94 | } 95 | } 96 | 97 | return result; 98 | } 99 | 100 | /** 101 | * Resolve rules (i.e. inheritance). 102 | */ 103 | function resolveParsedThemeRules( 104 | parsedThemeRules: ParsedThemeRule[], 105 | ): Theme { 106 | // Sort rules lexicographically, and then by index if necessary 107 | parsedThemeRules.sort((a: ParsedThemeRule, b: ParsedThemeRule) => { 108 | let r: number = strcmp(a.scope, b.scope); 109 | 110 | if (r !== 0) { 111 | return r; 112 | } 113 | 114 | r = strArrCmp(a.parentScopes, b.parentScopes); 115 | 116 | if (r !== 0) { 117 | return r; 118 | } 119 | 120 | return a.index - b.index; 121 | }); 122 | 123 | // Determine defaults 124 | let defaultParameters: T = {} as T; 125 | 126 | // pop up any rules of empty scope names and apply them together 127 | while (parsedThemeRules.length >= 1 && parsedThemeRules[0].scope === "") { 128 | // parsedThemeRules must be non-empty thus it should shift over here 129 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 130 | const incomingDefaults: ParsedThemeRule = parsedThemeRules.shift()!; 131 | 132 | // no harm to be cautious, do not trust ts types as one this theme would be injected from clients 133 | if (incomingDefaults.parameters) { 134 | defaultParameters = { 135 | ...defaultParameters, 136 | ...incomingDefaults.parameters, 137 | }; 138 | } 139 | } 140 | 141 | // create default tree element rule 142 | const defaults: ThemeTrieElementRule = new ThemeTrieElementRule(0, undefined, defaultParameters); 143 | 144 | // create tree root element 145 | const root: ThemeTrieElement = new ThemeTrieElement(new ThemeTrieElementRule(0, undefined, undefined), []); 146 | 147 | // append rules to the tree root 148 | parsedThemeRules.forEach((rule: ParsedThemeRule) => { 149 | root.insert(0, rule.scope, rule.parentScopes, rule.parameters); 150 | }); 151 | 152 | return new Theme(defaults, root); 153 | } 154 | 155 | /** 156 | * Theme tree element's rule holding 157 | * parent scopes 158 | * customized parameters record 159 | */ 160 | export class ThemeTrieElementRule { 161 | constructor(public scopeDepth: number, public readonly parentScopes: string[] | undefined, public parameters?: T) {} 162 | 163 | public static cloneArr( 164 | arr: ThemeTrieElementRule[], 165 | ): ThemeTrieElementRule[] { 166 | return arr.map((r: ThemeTrieElementRule) => r.clone()); 167 | } 168 | 169 | // for copy assignment 170 | public clone(): ThemeTrieElementRule { 171 | return new ThemeTrieElementRule(this.scopeDepth, this.parentScopes, this.parameters); 172 | } 173 | 174 | public acceptOverwrite(scopeDepth: number, parameters?: T): void { 175 | if (this.scopeDepth > scopeDepth) { 176 | throw PQP.Assert.isNever(this as never); 177 | } else { 178 | this.scopeDepth = scopeDepth; 179 | } 180 | 181 | // no harm to be safe over here 182 | if (parameters) { 183 | this.parameters = this.parameters ? { ...this.parameters, ...parameters } : { ...parameters }; 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * Theme tree element contains 190 | */ 191 | export class ThemeTrieElement { 192 | constructor( 193 | /** 194 | * Current rule of the element 195 | */ 196 | private readonly _mainRule: ThemeTrieElementRule, 197 | /** 198 | * Other rules of sharing the same scope name of the element but bear with parent scopes 199 | */ 200 | private readonly _rulesWithParentScopes: ThemeTrieElementRule[] = [], 201 | /** 202 | * Children rules beneath current element 203 | */ 204 | private readonly _children: Map> = new Map(), 205 | ) {} 206 | 207 | private static _sortBySpecificity( 208 | arr: ThemeTrieElementRule[], 209 | ): ThemeTrieElementRule[] { 210 | if (arr.length === 1) { 211 | return arr; 212 | } 213 | 214 | arr.sort(this._cmpBySpecificity); 215 | 216 | return arr; 217 | } 218 | 219 | private static _cmpBySpecificity( 220 | a: ThemeTrieElementRule, 221 | b: ThemeTrieElementRule, 222 | ): number { 223 | if (a.scopeDepth === b.scopeDepth) { 224 | const aParentScopes: string[] | undefined = a.parentScopes; 225 | const bParentScopes: string[] | undefined = b.parentScopes; 226 | const aParentScopesLen: number = !aParentScopes ? 0 : aParentScopes.length; 227 | const bParentScopesLen: number = !bParentScopes ? 0 : bParentScopes.length; 228 | 229 | if (aParentScopesLen === bParentScopesLen) { 230 | for (let i: number = 0; i < aParentScopesLen; i += 1) { 231 | // aParentScopes and bParentScopes cannot be empty as aParentScopesLen is larger than 0 232 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 233 | const aLen: number = aParentScopes![i].length; 234 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 235 | const bLen: number = bParentScopes![i].length; 236 | 237 | if (aLen !== bLen) { 238 | return bLen - aLen; 239 | } 240 | } 241 | } 242 | 243 | return bParentScopesLen - aParentScopesLen; 244 | } 245 | 246 | return b.scopeDepth - a.scopeDepth; 247 | } 248 | 249 | private getScopeHeadTailPair(scope: string): [string, string] { 250 | const dotIndex: number = scope.indexOf("."); 251 | let head: string; 252 | let tail: string; 253 | 254 | if (dotIndex === -1) { 255 | head = scope; 256 | tail = ""; 257 | } else { 258 | head = scope.substring(0, dotIndex); 259 | tail = scope.substring(dotIndex + 1); 260 | } 261 | 262 | return [head, tail]; 263 | } 264 | 265 | public match(scope: string): ThemeTrieElementRule[] { 266 | if (scope === "") { 267 | // hit the tree rule of an empty scope name 268 | return ThemeTrieElement._sortBySpecificity([this._mainRule].concat(this._rulesWithParentScopes)); 269 | } 270 | 271 | const [head, tail]: [string, string] = this.getScopeHeadTailPair(scope); 272 | const oneChild: ThemeTrieElement | undefined = this._children.get(head); 273 | 274 | if (oneChild) { 275 | return oneChild.match(tail); 276 | } 277 | 278 | // return current rules which should be the mostly matched 279 | return ThemeTrieElement._sortBySpecificity([this._mainRule].concat(this._rulesWithParentScopes)); 280 | } 281 | 282 | public insert(scopeDepth: number, scope: string, parentScopes: string[] | undefined, parameters?: T): void { 283 | if (scope === "") { 284 | this._doInsertHere(scopeDepth, parentScopes, parameters); 285 | 286 | return; 287 | } 288 | 289 | const [head, tail]: [string, string] = this.getScopeHeadTailPair(scope); 290 | let child: ThemeTrieElement; 291 | 292 | const oneChild: ThemeTrieElement | undefined = this._children.get(head); 293 | 294 | if (oneChild) { 295 | child = oneChild; 296 | } else { 297 | child = new ThemeTrieElement( 298 | this._mainRule.clone(), 299 | ThemeTrieElementRule.cloneArr(this._rulesWithParentScopes), 300 | ); 301 | 302 | this._children.set(head, child); 303 | } 304 | 305 | child.insert(scopeDepth + 1, tail, parentScopes, parameters); 306 | } 307 | 308 | private _doInsertHere(scopeDepth: number, parentScopes: string[] | undefined, parameters?: T): void { 309 | if (!parentScopes) { 310 | // merge into the main rule 311 | this._mainRule.acceptOverwrite(scopeDepth, parameters); 312 | 313 | return; 314 | } 315 | 316 | // try to merge into existing one rule w/ parent scopes 317 | for (let i: number = 0; i < this._rulesWithParentScopes.length; i += 1) { 318 | const rule: ThemeTrieElementRule = this._rulesWithParentScopes[i]; 319 | 320 | if (strArrCmp(rule.parentScopes, parentScopes) === 0) { 321 | // gotcha, we gonna merge this into an existing one 322 | rule.acceptOverwrite(scopeDepth, parameters); 323 | 324 | return; 325 | } 326 | } 327 | 328 | // cannot find an existing rule w/ parent scopes 329 | this._rulesWithParentScopes.push(new ThemeTrieElementRule(scopeDepth, parentScopes, parameters)); 330 | } 331 | } 332 | 333 | /** 334 | * Theme object supports style tokens 335 | */ 336 | export class Theme { 337 | public static createFromRawTheme(source?: IRawTheme): Theme { 338 | return this.createFromParsedTheme(parseTheme(source)); 339 | } 340 | 341 | public static createFromParsedTheme(source: ParsedThemeRule[]): Theme { 342 | return resolveParsedThemeRules(source); 343 | } 344 | 345 | private readonly _cache: Map[]>; 346 | 347 | constructor(private readonly _defaults: ThemeTrieElementRule, private readonly _root: ThemeTrieElement) { 348 | this._cache = new Map(); 349 | } 350 | 351 | public getDefaults(): ThemeTrieElementRule { 352 | return this._defaults; 353 | } 354 | 355 | /** 356 | * Find the array of matched rules for the scope name 357 | * @param scopeName: a string like "segment1.segment2.segment3" 358 | */ 359 | public match(scopeName: string): ThemeTrieElementRule[] { 360 | let matchedRuleArr: ThemeTrieElementRule[]; 361 | const cachedMatchedRuleArr: ThemeTrieElementRule[] | undefined = this._cache.get(scopeName); 362 | 363 | if (cachedMatchedRuleArr) { 364 | matchedRuleArr = cachedMatchedRuleArr; 365 | } else { 366 | matchedRuleArr = this._root.match(scopeName); 367 | this._cache.set(scopeName, matchedRuleArr); 368 | } 369 | 370 | return matchedRuleArr; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import { ThemeTrieElementRule } from "./themes"; 5 | 6 | export type IParameters = Record; 7 | 8 | /** 9 | * A single theme setting. 10 | */ 11 | export interface IRawThemeSetting { 12 | readonly name?: string; 13 | /** 14 | * hierarchic scope list seperated by comma like: 15 | * eg: 16 | * "scopeList1,scopeList2,scopeList3,scopeList4" 17 | * or 18 | * ["scopeList1", "scopeList2", "scopeList3", "scopeList4"] 19 | * 20 | * and one scopeList could also be consisted of multiple scopes seperated by space like: 21 | * eg: 22 | * "scope1 scope2 scope3 scope4" 23 | * "scope1> scope2 scope3 scope4" 24 | */ 25 | readonly scope?: string | ReadonlyArray; 26 | readonly parameters: T; 27 | } 28 | 29 | /** 30 | * A raw theme of multiple raw theme settings 31 | */ 32 | export interface IRawTheme { 33 | readonly name?: string; 34 | readonly settings: IRawThemeSetting[]; 35 | } 36 | 37 | /** 38 | * Theme provider 39 | */ 40 | export interface IThemeProvider { 41 | readonly themeMatch: (scopeName: string) => ThemeTrieElementRule[]; 42 | readonly getDefaults: () => ThemeTrieElementRule; 43 | } 44 | 45 | /** 46 | * Registry options 47 | */ 48 | export interface RegistryOptions { 49 | readonly theme: IRawTheme; 50 | // we could add tracer over here if we want 51 | } 52 | -------------------------------------------------------------------------------- /src/powerquery-formatter/themes/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export function strcmp(a: string, b: string): number { 5 | if (a < b) { 6 | return -1; 7 | } 8 | 9 | if (a > b) { 10 | return 1; 11 | } 12 | 13 | return 0; 14 | } 15 | 16 | export function strArrCmp(a: string[] | undefined, b: string[] | undefined): number { 17 | const hasA: boolean = Array.isArray(a); 18 | const hasB: boolean = Array.isArray(b); 19 | 20 | if (!hasA && !hasB) { 21 | return 0; 22 | } 23 | 24 | if (!hasA) { 25 | return -1; 26 | } 27 | 28 | if (!hasB) { 29 | return 1; 30 | } 31 | 32 | // a and b both mush be array 33 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 34 | const len1: number = a!.length; 35 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 36 | const len2: number = b!.length; 37 | 38 | if (len1 === len2) { 39 | // eslint-disable-next-line no-plusplus 40 | for (let i: number = 0; i < len1; i++) { 41 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 42 | const res: number = strcmp(a![i], b![i]); 43 | 44 | if (res !== 0) { 45 | return res; 46 | } 47 | } 48 | 49 | return 0; 50 | } 51 | 52 | return len1 - len2; 53 | } 54 | -------------------------------------------------------------------------------- /src/powerquery-formatter/trace.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | export enum FormatTraceConstant { 5 | IsMultiline = "Format.IsMultiline", 6 | IsMultilinePhase1 = "Format.IsMultilinePhase1", 7 | IsMultilinePhase2 = "Format.IsMultilinePhase2", 8 | Format = "Format", 9 | LinearLength = "Format.LinearLength", 10 | SerializeParameter = "Format.SerializeParameter", 11 | PreProcessParameter = "Format.PreProcessParameter", 12 | } 13 | -------------------------------------------------------------------------------- /src/scripts/recursiveDirectoryFormat.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import * as fs from "fs"; 5 | import * as path from "path"; 6 | import { DefaultSettings, TriedFormat, tryFormat } from "../powerquery-formatter"; 7 | import { ResultUtils } from "@microsoft/powerquery-parser"; 8 | 9 | interface CliArgs { 10 | readonly allowedExtensions: ReadonlySet; 11 | readonly rootDirectoryPath: string; 12 | } 13 | 14 | const DefaultCliArgs: Pick = { 15 | allowedExtensions: new Set([".mout"]), 16 | }; 17 | 18 | function getPowerQueryFilePathsRecursively( 19 | directoryPath: string, 20 | allowedExtensions: ReadonlySet, 21 | ): ReadonlyArray { 22 | let files: ReadonlyArray = getSubDirectoryPaths(directoryPath) 23 | // go through each directory 24 | .map((subDirectoryPath: string) => getPowerQueryFilePathsRecursively(subDirectoryPath, allowedExtensions)) 25 | // map returns a 2d array (array of file arrays) so flatten 26 | .reduce((a: ReadonlyArray, b: ReadonlyArray) => a.concat(b), []); 27 | 28 | // Get files in directoryPath 29 | files = files.concat(getPowerQueryFilePaths(directoryPath, allowedExtensions)); 30 | 31 | return files; 32 | } 33 | 34 | function readContents(filePath: string): string { 35 | // tslint:disable-next-line: non-literal-fs-path 36 | const contents: string = fs.readFileSync(filePath, "utf8"); 37 | 38 | return contents.replace(/^\uFEFF/, ""); 39 | } 40 | 41 | function writeContents(filePath: string, contents: string): void { 42 | const dirPath: string = path.dirname(filePath); 43 | 44 | // tslint:disable-next-line: non-literal-fs-path 45 | if (!fs.existsSync(dirPath)) { 46 | // tslint:disable-next-line: non-literal-fs-path 47 | fs.mkdirSync(dirPath, { recursive: true }); 48 | } 49 | 50 | // tslint:disable-next-line: non-literal-fs-path 51 | fs.writeFileSync(filePath, contents, { encoding: "utf8" }); 52 | } 53 | 54 | function isDirectory(path: string): boolean { 55 | // tslint:disable-next-line: non-literal-fs-path 56 | try { 57 | return fs.statSync(path).isDirectory(); 58 | } catch { 59 | return false; 60 | } 61 | } 62 | 63 | function isFile(filePath: string): boolean { 64 | // tslint:disable-next-line: non-literal-fs-path 65 | return fs.statSync(filePath).isFile(); 66 | } 67 | 68 | function isPowerQueryFile(filePath: string, allowedExtensions: ReadonlySet): boolean { 69 | return isFile(filePath) && isPowerQueryExtension(path.extname(filePath), allowedExtensions); 70 | } 71 | 72 | function isPowerQueryExtension(extension: string, allowedExtensions: ReadonlySet): boolean { 73 | return allowedExtensions.has(extension); 74 | } 75 | 76 | function getSubDirectoryPaths(rootDirectory: string): ReadonlyArray { 77 | // tslint:disable-next-line: non-literal-fs-path 78 | return ( 79 | fs 80 | // tslint:disable-next-line: non-literal-fs-path 81 | .readdirSync(rootDirectory) 82 | .map((name: string) => path.join(rootDirectory, name)) 83 | .filter(isDirectory) 84 | ); 85 | } 86 | 87 | function getPowerQueryFilePaths(filePath: string, allowedExtensions: ReadonlySet): ReadonlyArray { 88 | // tslint:disable-next-line: non-literal-fs-path 89 | return ( 90 | fs 91 | // tslint:disable-next-line: non-literal-fs-path 92 | .readdirSync(filePath) 93 | .map((name: string) => path.join(filePath, name)) 94 | .filter((filePath: string) => isPowerQueryFile(filePath, allowedExtensions)) 95 | ); 96 | } 97 | 98 | function printText(text: string): void { 99 | if (process.stdout.isTTY) { 100 | process.stdout.write(text); 101 | } else { 102 | // Remove trailing newline as console.log already adds one. 103 | console.log(text.replace(/\n$/, "")); 104 | } 105 | } 106 | 107 | function getCliArgs(): CliArgs { 108 | const args: ReadonlyArray = process.argv; 109 | 110 | if (args.length <= 2) { 111 | const errorMessage: string = 112 | "No arguments provided. Expecting a directory path and optionally a CSV of extensions to format. Aborting.\n"; 113 | 114 | printText(errorMessage); 115 | throw new Error(errorMessage); 116 | } 117 | 118 | if (args.length === 3) { 119 | const allowedExtensions: ReadonlySet = DefaultCliArgs.allowedExtensions; 120 | 121 | printText( 122 | `No argument given for allowed extensions. Defaulting to [${Array.from(allowedExtensions).join(", ")}].\n`, 123 | ); 124 | 125 | return { 126 | allowedExtensions, 127 | rootDirectoryPath: validateDirectoryGetArg(args[2]), 128 | }; 129 | } 130 | 131 | if (args.length === 4) { 132 | return { 133 | allowedExtensions: validateExtensionsGetArg(args[3]), 134 | rootDirectoryPath: validateDirectoryGetArg(args[2]), 135 | }; 136 | } 137 | 138 | const errorMessage: string = "Too many arguments provided. Aborting.\n"; 139 | printText(errorMessage); 140 | throw new Error(errorMessage); 141 | } 142 | 143 | function validateDirectoryGetArg(potentialDirectoryPath: string | undefined): string { 144 | if (!potentialDirectoryPath || !isDirectory(potentialDirectoryPath)) { 145 | const errorMessage: string = `Provided argument is not a directory path: "${potentialDirectoryPath}". Aborting.\n`; 146 | printText(errorMessage); 147 | throw new Error(errorMessage); 148 | } 149 | 150 | return potentialDirectoryPath; 151 | } 152 | 153 | function validateExtensionsGetArg(potentialExtensions: string | undefined): ReadonlySet { 154 | const result: Set = new Set(); 155 | 156 | for (const potentialExtension of potentialExtensions?.split(",") ?? []) { 157 | if (!potentialExtension.startsWith(".")) { 158 | const errorMessage: string = `Provided extension is not valid: "${potentialExtension}", expecting a CSV such as ".pq,.mout" Aborting.\n`; 159 | printText(errorMessage); 160 | throw new Error(errorMessage); 161 | } 162 | 163 | result.add(potentialExtension); 164 | } 165 | 166 | return result; 167 | } 168 | 169 | async function main(): Promise { 170 | const { allowedExtensions, rootDirectoryPath }: CliArgs = getCliArgs(); 171 | 172 | const powerQueryFilePaths: ReadonlyArray = getPowerQueryFilePathsRecursively( 173 | rootDirectoryPath, 174 | allowedExtensions, 175 | ); 176 | 177 | if (powerQueryFilePaths.length === 0) { 178 | printText(`No files found with extensions [${Array.from(allowedExtensions).join(", ")}].\n`); 179 | 180 | return; 181 | } 182 | 183 | for (const filePath of powerQueryFilePaths) { 184 | // eslint-disable-next-line no-await-in-loop 185 | const triedFormat: TriedFormat = await tryFormat(DefaultSettings, readContents(filePath)); 186 | 187 | if (ResultUtils.isOk(triedFormat)) { 188 | printText(`Formatted "${filePath}" successfully\n`); 189 | writeContents(filePath, triedFormat.value); 190 | } else { 191 | printText(`Failed to format "${filePath}":\n`); 192 | } 193 | } 194 | } 195 | 196 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 197 | (async (): Promise => { 198 | void (await main()); 199 | })(); 200 | -------------------------------------------------------------------------------- /src/test/comments.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import "mocha"; 5 | import { compare, expectFormat } from "./common"; 6 | 7 | describe("comment serialize", () => { 8 | // -------------------------------------- 9 | // ---------- RecordExpression ---------- 10 | // -------------------------------------- 11 | describe("RecordExpression", () => { 12 | it("[ /*foo*/ key1=value1, key2=value2 ]", async () => { 13 | const expected: string = `[/*foo*/ key1 = value1, key2 = value2]`; 14 | const actual: string = await expectFormat("[ /*foo*/ key1=value1, key2=value2 ]"); 15 | 16 | compare(expected, actual); 17 | }); 18 | 19 | it("[ /*foo*//*bar*/ key1=value1, key2=value2 ]", async () => { 20 | const expected: string = `[/*foo*/ /*bar*/ key1 = value1, key2 = value2]`; 21 | const actual: string = await expectFormat("[ /*foo*//*bar*/ key1=value1, key2=value2 ]"); 22 | 23 | compare(expected, actual); 24 | }); 25 | 26 | it("[ key1=/*foo*/value1, key2=value2 ]", async () => { 27 | const expected: string = `[key1 = /*foo*/ value1, key2 = value2]`; 28 | const actual: string = await expectFormat("[ key1=/*foo*/value1, key2=value2 ]"); 29 | 30 | compare(expected, actual); 31 | }); 32 | 33 | it("[ // foo\\n key1=value1 ]", async () => { 34 | const expected: string = ` 35 | [ 36 | // foo 37 | key1 = value1] 38 | `; 39 | 40 | const actual: string = await expectFormat("[ // foo\n key1=value1 ]"); 41 | compare(expected, actual); 42 | }); 43 | 44 | it("[ // foo\\n // bar \\n key1=value1 ]", async () => { 45 | const expected: string = ` 46 | [ 47 | // foo 48 | // bar 49 | key1 = value1] 50 | `; 51 | 52 | const actual: string = await expectFormat("[ // foo\n // bar\n key1=value1 ]"); 53 | 54 | compare(expected, actual); 55 | }); 56 | 57 | it("[ /* foo */ // bar\\n key1=value1 ]", async () => { 58 | const expected: string = ` 59 | [/* foo */ // bar 60 | key1 = value1] 61 | `; 62 | 63 | const actual: string = await expectFormat("[ /* foo */ // bar\n key1=value1 ]"); 64 | 65 | compare(expected, actual); 66 | }); 67 | 68 | it("[ /* foo */ // bar\\n /* foobar */ key1=value1 ]", async () => { 69 | const expected: string = ` 70 | [/* foo */ // bar 71 | /* foobar */ key1 = value1] 72 | `; 73 | 74 | const actual: string = await expectFormat("[ /* foo */ // bar\n /* foobar */ key1=value1 ]"); 75 | 76 | compare(expected, actual); 77 | }); 78 | 79 | it("[ key1 = // foo\\n value1 ]", async () => { 80 | const expected: string = ` 81 | [key1 = 82 | // foo 83 | value1] 84 | `; 85 | 86 | const actual: string = await expectFormat("[ key1 = // foo\n value1 ]"); 87 | compare(expected, actual); 88 | }); 89 | 90 | it("[ key1 // foo\\n = value1 ]", async () => { 91 | const expected: string = ` 92 | [key1 93 | // foo 94 | = value1] 95 | `; 96 | 97 | const actual: string = await expectFormat("[ key1 // foo\n = value1 ]"); 98 | compare(expected, actual); 99 | }); 100 | 101 | it("section foobar; x = 1; // lineComment\n y = 1;", async () => { 102 | const expected: string = ` 103 | section foobar; 104 | 105 | x = 1; 106 | // lineComment 107 | y = 1; 108 | `; 109 | 110 | const actual: string = await expectFormat("section foobar; x = 1; // lineComment\n y = 1;"); 111 | 112 | compare(expected, actual); 113 | }); 114 | 115 | it("let y = 1 in y // stuff", async () => { 116 | const expected: string = ` 117 | let y = 1 in y 118 | // stuff 119 | `; 120 | 121 | const actual: string = await expectFormat("let y = 1 in y // stuff"); 122 | 123 | compare(expected, actual); 124 | }); 125 | 126 | it("let y = 1 in y /* foo */ ", async () => { 127 | const expected: string = ` 128 | let y = 1 in y/* foo */ 129 | `; 130 | 131 | const actual: string = await expectFormat("let y = 1 in y /* foo */"); 132 | 133 | compare(expected, actual); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /src/test/common.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import "mocha"; 5 | import * as PQP from "@microsoft/powerquery-parser"; 6 | import { expect } from "chai"; 7 | 8 | import { FormatSettings, IndentationLiteral, NewlineLiteral, TriedFormat, tryFormat } from ".."; 9 | 10 | const DefaultFormatSettings: FormatSettings = { 11 | ...PQP.DefaultSettings, 12 | indentationLiteral: IndentationLiteral.SpaceX4, 13 | maxWidth: 120, 14 | newlineLiteral: NewlineLiteral.Unix, 15 | }; 16 | 17 | export function compare(expected: string, actual: string, newlineLiteral: NewlineLiteral = NewlineLiteral.Unix): void { 18 | expected = expected.trim(); 19 | actual = actual.trim(); 20 | const actualLines: ReadonlyArray = actual.split(newlineLiteral); 21 | const expectedLines: ReadonlyArray = expected.split(newlineLiteral); 22 | 23 | const minLength: number = Math.min(actualLines.length, expectedLines.length); 24 | 25 | for (let lineNumber: number = 0; lineNumber < minLength; lineNumber += 1) { 26 | const actualLine: string = actualLines[lineNumber]; 27 | const expectedLine: string = expectedLines[lineNumber]; 28 | 29 | if (expectedLine !== actualLine) { 30 | expect(actualLine).to.equal( 31 | expectedLine, 32 | JSON.stringify( 33 | { 34 | lineNumber, 35 | expectedLine, 36 | actualLine, 37 | expected, 38 | actual, 39 | }, 40 | undefined, 41 | 4, 42 | ), 43 | ); 44 | } 45 | } 46 | 47 | const edgeExpectedLine: string = expectedLines[minLength]; 48 | const edgeActualLine: string = actualLines[minLength]; 49 | expect(edgeActualLine).to.equal(edgeExpectedLine, `line:${minLength + 1}`); 50 | } 51 | 52 | // Formats the text twice to ensure the formatter emits the same tokens. 53 | export async function expectFormat( 54 | text: string, 55 | formatSettings: FormatSettings = DefaultFormatSettings, 56 | ): Promise { 57 | text = text.trim(); 58 | const firstTriedFormat: TriedFormat = await tryFormat(formatSettings, text); 59 | 60 | if (PQP.ResultUtils.isError(firstTriedFormat)) { 61 | throw firstTriedFormat.error; 62 | } 63 | 64 | const firstOk: string = firstTriedFormat.value; 65 | 66 | const secondTriedFormat: TriedFormat = await tryFormat(formatSettings, firstOk); 67 | 68 | if (PQP.ResultUtils.isError(secondTriedFormat)) { 69 | throw secondTriedFormat.error; 70 | } 71 | 72 | const secondOk: string = secondTriedFormat.value; 73 | 74 | compare(firstOk, secondOk); 75 | 76 | return firstOk; 77 | } 78 | -------------------------------------------------------------------------------- /src/test/mochaConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, mocha-junit-reporter", 3 | "mochaJunitReporterReporterOptions": { 4 | "mochaFile": "test-results.xml" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/test/section.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import "mocha"; 5 | import { compare, expectFormat } from "./common"; 6 | 7 | describe("section", () => { 8 | describe("Section", () => { 9 | it("section;", async () => { 10 | const expected: string = `section;`; 11 | const actual: string = await expectFormat(`section;`); 12 | compare(expected, actual); 13 | }); 14 | 15 | it("section name;", async () => { 16 | const expected: string = `section name;`; 17 | const actual: string = await expectFormat(`section name;`); 18 | compare(expected, actual); 19 | }); 20 | 21 | it("[] section name;", async () => { 22 | const expected: string = ` 23 | [] 24 | section name; 25 | `; 26 | 27 | const actual: string = await expectFormat(`[] section name;`); 28 | compare(expected, actual); 29 | }); 30 | 31 | it("[] section;", async () => { 32 | const expected: string = ` 33 | [] 34 | section; 35 | `; 36 | 37 | const actual: string = await expectFormat(`[] section;`); 38 | compare(expected, actual); 39 | }); 40 | 41 | it("[a = 1] section;", async () => { 42 | const expected: string = ` 43 | [a = 1] 44 | section; 45 | `; 46 | 47 | const actual: string = await expectFormat(`[a = 1] section;`); 48 | compare(expected, actual); 49 | }); 50 | 51 | it("[a = {}] section;", async () => { 52 | const expected: string = ` 53 | [a = {}] 54 | section; 55 | `; 56 | 57 | const actual: string = await expectFormat(`[a = {}] section;`); 58 | compare(expected, actual); 59 | }); 60 | 61 | it("[a = 1, b = 2] section;", async () => { 62 | const expected: string = ` 63 | [a = 1, b = 2] 64 | section; 65 | `; 66 | 67 | const actual: string = await expectFormat(`[a=1, b=2] section;`); 68 | compare(expected, actual); 69 | }); 70 | 71 | it("[a = {}, b = {}] section;", async () => { 72 | const expected: string = ` 73 | [a = {}, b = {}] 74 | section; 75 | `; 76 | 77 | const actual: string = await expectFormat(`[a = {}, b = {}] section;`); 78 | compare(expected, actual); 79 | }); 80 | 81 | it("[a = {1}, b = {2}] section;", async () => { 82 | const expected: string = ` 83 | [a = {1}, b = {2}] 84 | section; 85 | `; 86 | 87 | const actual: string = await expectFormat(`[a = {1}, b = {2}] section;`); 88 | compare(expected, actual); 89 | }); 90 | 91 | it("[a = 1, b = [c = {2, 3, 4}], e = 5] section;", async () => { 92 | const expected: string = ` 93 | [a = 1, b = [c = {2, 3, 4}], e = 5] 94 | section; 95 | `; 96 | 97 | const actual: string = await expectFormat(`[a = 1, b = [c = {2, 3, 4}], e = 5] section;`); 98 | 99 | compare(expected, actual); 100 | }); 101 | }); 102 | 103 | describe("SectionMember", () => { 104 | it("section; x = 1;", async () => { 105 | const expected: string = ` 106 | section; 107 | 108 | x = 1; 109 | `; 110 | 111 | const actual: string = await expectFormat(`section; x = 1;`); 112 | compare(expected, actual); 113 | }); 114 | 115 | it("section; [] x = 1;", async () => { 116 | const expected: string = ` 117 | section; 118 | 119 | [] x = 1; 120 | `; 121 | 122 | const actual: string = await expectFormat(`section; [] x = 1;`); 123 | compare(expected, actual); 124 | }); 125 | 126 | it("section; [a=1, b=2] x = 1;", async () => { 127 | const expected: string = ` 128 | section; 129 | 130 | [a = 1, b = 2] x = 1; 131 | `; 132 | 133 | const actual: string = await expectFormat(`section; [a=1, b=2] x = 1;`); 134 | compare(expected, actual); 135 | }); 136 | 137 | it("section; [a=1, b=2] shared x = 1;", async () => { 138 | const expected: string = ` 139 | section; 140 | 141 | [a = 1, b = 2] 142 | shared x = 1; 143 | `; 144 | 145 | const actual: string = await expectFormat(`section; [a=1, b=2] shared x = 1;`); 146 | 147 | compare(expected, actual); 148 | }); 149 | 150 | it("section; [a = 1] x = 1;", async () => { 151 | const expected: string = ` 152 | section; 153 | 154 | [a = 1] x = 1; 155 | `; 156 | 157 | const actual: string = await expectFormat(`section; [a = 1] x = 1;`); 158 | compare(expected, actual); 159 | }); 160 | 161 | it("section; [a = 1] shared x = 1;", async () => { 162 | const expected: string = ` 163 | section; 164 | 165 | [a = 1] 166 | shared x = 1; 167 | `; 168 | 169 | const actual: string = await expectFormat(`section; [a = 1] shared x = 1;`); 170 | 171 | compare(expected, actual); 172 | }); 173 | 174 | it("section; x = 1; y = 2;", async () => { 175 | const expected: string = ` 176 | section; 177 | 178 | x = 1; 179 | y = 2; 180 | `; 181 | 182 | const actual: string = await expectFormat(`section; x = 1; y = 2;`); 183 | compare(expected, actual); 184 | }); 185 | 186 | it("section; Other = 3; Constant.Alpha = 1; Constant.Beta = 2; Other = 3;", async () => { 187 | const expected: string = ` 188 | section; 189 | 190 | Other = 3; 191 | 192 | Constant.Alpha = 1; 193 | Constant.Beta = 2; 194 | 195 | Other = 3; 196 | `; 197 | 198 | const actual: string = await expectFormat( 199 | `section; Other = 3; Constant.Alpha = 1; Constant.Beta = 2; Other = 3;`, 200 | ); 201 | 202 | compare(expected, actual); 203 | }); 204 | 205 | it("Lengthy record in section", async () => { 206 | const expected: string = ` 207 | [Version = "1.1.0"] 208 | section UserVoice; 209 | 210 | SuggestionsTable = 211 | let 212 | descriptor = #table( 213 | { 214 | { 215 | false, 216 | "category", 217 | "category_id", 218 | type nullable Int64.Type, 219 | { 220 | [ 221 | operator = "Equals", 222 | value = (value) => if value is null then "uncategorized" else Text.From(value) 223 | ] 224 | } 225 | } 226 | } 227 | ) 228 | in 229 | descriptor; 230 | `; 231 | 232 | const actual: string = await expectFormat( 233 | `[Version="1.1.0"] 234 | section UserVoice; 235 | 236 | SuggestionsTable = let 237 | descriptor = #table( 238 | { 239 | { 240 | false, 241 | "category", 242 | "category_id", 243 | type nullable Int64.Type, 244 | {[operator = "Equals", value = (value) => if value is null then "uncategorized" else Text.From(value)]} 245 | } 246 | } 247 | ) 248 | in 249 | descriptor; 250 | `, 251 | ); 252 | 253 | compare(expected, actual); 254 | }); 255 | }); 256 | }); 257 | -------------------------------------------------------------------------------- /src/test/smallPrograms.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft Corporation. 2 | // Licensed under the MIT license. 3 | 4 | import "mocha"; 5 | import { compare, expectFormat } from "./common"; 6 | 7 | describe(`small programs`, () => { 8 | it(`fastPow`, async () => { 9 | const expected: string = ` 10 | // taken from: https://en.wikipedia.org/wiki/Exponentiation_by_squaring 11 | // removed negative powers, sure to have bugs 12 | // 13 | // Function exp_by_squaring(x, n) 14 | // if n < 0 then return exp_by_squaring(1 / x, -n); 15 | // else if n = 0 then return 1; 16 | // else if n = 1 then return x ; 17 | // else if n is even then return exp_by_squaring(x * x, n / 2); 18 | // else if n is odd then return x * exp_by_squaring(x * x, (n - 1) / 2); 19 | let 20 | isEven = (x as number) => Number.Mod(x, 2) = 0, 21 | pow = (x as number, p as number) => 22 | if p = 0 then 23 | 1 24 | else if p < 0 then 25 | error "negative power not supported" 26 | else 27 | x * @pow(x, p - 1), 28 | fastPow = (x as number, p as number) => 29 | if p = 0 then 30 | 1 31 | else if p < 0 then 32 | error "negative power not supported" 33 | else if isEven(p) then 34 | @fastPow(x * x, p / 2) 35 | else 36 | x * @fastPow(x * x, (p - 1) / 2) 37 | in 38 | fastPow(2, 8) 39 | `; 40 | 41 | const actual: string = await expectFormat( 42 | ` 43 | // taken from: https://en.wikipedia.org/wiki/Exponentiation_by_squaring 44 | // removed negative powers, sure to have bugs 45 | // 46 | // Function exp_by_squaring(x, n) 47 | // if n < 0 then return exp_by_squaring(1 / x, -n); 48 | // else if n = 0 then return 1; 49 | // else if n = 1 then return x ; 50 | // else if n is even then return exp_by_squaring(x * x, n / 2); 51 | // else if n is odd then return x * exp_by_squaring(x * x, (n - 1) / 2); 52 | let 53 | isEven = (x as number) => Number.Mod(x, 2) = 0, 54 | pow = (x as number, p as number) => 55 | if p = 0 then 56 | 1 57 | else if p < 0 then 58 | error "negative power not supported" 59 | else 60 | x * @pow(x, p - 1), 61 | fastPow = (x as number, p as number) => 62 | if p = 0 then 63 | 1 64 | else if p < 0 then 65 | error "negative power not supported" 66 | else if isEven(p) then 67 | @fastPow(x * x, p / 2) 68 | else 69 | x * @fastPow(x * x, (p - 1) / 2) 70 | in 71 | fastPow(2, 8) 72 | `, 73 | ); 74 | 75 | compare(expected, actual); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "downlevelIteration": true, 5 | "incremental": true, 6 | "module": "commonjs", 7 | "noEmitOnError": true, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitOverride": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "lib", 14 | "resolveJsonModule": true, 15 | "rootDir": "src", 16 | "sourceMap": true, 17 | "strict": true, 18 | "target": "es6", 19 | "tsBuildInfoFile": "./tsconfig.tsbuildinfo" 20 | }, 21 | "exclude": ["node_modules"], 22 | "include": ["src/**/*.ts"], 23 | } 24 | --------------------------------------------------------------------------------