├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENCE ├── README.md ├── client └── client.ts ├── images ├── vscode-phpmd.png └── vscode-phpmd.svg ├── mocha.opts ├── package.json ├── phpmd └── phpmd.phar ├── server ├── Server.ts ├── controller │ └── PhpmdController.ts ├── factory │ ├── BuildDiagnosticsTaskFactory.ts │ ├── ClientConnectionNotifierFactory.ts │ ├── ExecuteProcessTaskFactory.ts │ ├── IFactory.ts │ ├── ILoggerFactory.ts │ ├── INotifierFactory.ts │ ├── NullLoggerFactory.ts │ ├── NullNotifierFactory.ts │ ├── ParseTaskFactory.ts │ ├── PhpmdControllerFactory.ts │ ├── PipelineFactory.ts │ ├── PipelinePayloadFactory.ts │ ├── RemoteConsoleLoggerFactory.ts │ └── TestFileTaskFactory.ts ├── init.ts ├── model │ ├── IPhpmdEnvironmentModel.ts │ ├── IPhpmdSettingsModel.ts │ ├── PipelineErrorModel.ts │ ├── PipelinePayloadModel.ts │ └── pmd │ │ ├── IPmd.ts │ │ ├── IPmdFileData.ts │ │ ├── IPmdFileMetaData.ts │ │ ├── IPmdMetaData.ts │ │ ├── IPmdViolation.ts │ │ ├── IPmdViolationMetaData.ts │ │ └── index.ts └── service │ ├── PhpmdCommandBuilder.ts │ ├── PhpmdService.ts │ ├── logger │ ├── ILogger.ts │ ├── NullLogger.ts │ └── RemoteConsoleLogger.ts │ ├── notifier │ ├── ClientConnectionNotifier.ts │ ├── INotifier.ts │ └── NullNotifier.ts │ └── pipeline │ ├── BuildDiagnosticsStrategy.ts │ ├── ExecuteProcessStrategy.ts │ ├── ParseStrategy.ts │ └── TestFileStrategy.ts ├── tests ├── ServerTest.ts ├── controller │ └── PhpmdControllerTest.ts ├── factory │ ├── BuildDiagnosticsTaskFactoryTest.ts │ ├── ClientConnectionNotifierFactoryTest.ts │ ├── ExecuteProcessTaskFactoryTest.ts │ ├── NullLoggerFactoryTest.ts │ ├── NullNotifierFactoryTest.ts │ ├── ParseTaskFactoryTest.ts │ ├── PhpmdControllerFactoryTest.ts │ ├── PipelineFactoryTest.ts │ ├── PipelinePayloadFactoryTest.ts │ ├── RemoteConsoleLoggerFactoryTest.ts │ └── TestFileTaskFactoryTest.ts ├── model │ └── PipelinePayloadModelTest.ts └── service │ ├── PhpmdCommandBuilderTest.ts │ ├── PhpmdServiceTest.ts │ ├── logger │ ├── NullLoggerTest.ts │ └── RemoteConsoleLoggerTest.ts │ ├── notifier │ ├── ClientConnectionNotifierTest.ts │ └── NullNotifierTest.ts │ └── pipeline │ ├── BuildDiagnosticsStrategyTest.ts │ ├── ExecuteProcessStrategyTest.ts │ ├── ParseStrategyTest.ts │ └── TestFileStrategyTest.ts ├── tsconfig.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [{src,scripts}/**.{ts,json,js}] 4 | end_of_line = crlf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | client/server 4 | .nyc_output 5 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | before_install: 5 | - sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg 6 | - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 7 | - sudo apt-get update -qq 8 | - sudo apt-get install -y -qq yarn=1.19.2-1 9 | cache: 10 | yarn: true 11 | script: 12 | - yarn install 13 | - npm test 14 | - npm run report-coverage 15 | after_success: 16 | - cat ./coverage/coverage-final.json | ./node_modules/.bin/codecov -------------------------------------------------------------------------------- /.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.1.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Language Provider", 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceRoot}" 14 | ], 15 | "preLaunchTask": "build", 16 | "stopOnEntry": false, 17 | "sourceMaps": true, 18 | "outFiles": [ 19 | "${workspaceRoot}/out/client/**/*.js" 20 | ] 21 | }, 22 | { 23 | "name": "Attach Language Provider Server", 24 | "type": "node", 25 | "request": "attach", 26 | "protocol": "inspector", 27 | "sourceMaps": true, 28 | "outFiles": [ 29 | "${workspaceRoot}/out/server/**/*.js" 30 | ], 31 | "timeout": 50000 // Allow for build task to finish 32 | }, 33 | { 34 | "type": "node", 35 | "request": "launch", 36 | "name": "Launch unit tests", 37 | "cwd": "${workspaceRoot}", 38 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha", 39 | "runtimeArgs": [ 40 | "--opts", "mocha.opts" 41 | ], 42 | "stopOnEntry": false, 43 | "sourceMaps": true 44 | } 45 | ], 46 | "compounds": [ 47 | { 48 | "name": "Launch Language Provider & Attach to Server", 49 | "configurations": [ 50 | "Launch Language Provider", 51 | "Attach Language Provider Server" 52 | ] 53 | } 54 | ] 55 | } -------------------------------------------------------------------------------- /.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": "npm", 9 | "script": "compile", 10 | "problemMatcher": [] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/*.ts 2 | **/tsconfig.json 3 | .nyc_output/** 4 | .vscode/** 5 | coverage/** 6 | .editorconfig 7 | .gitignore 8 | mocha.opts 9 | tslint.json 10 | .travis.yml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 1.3.0 2 | 3 | * Added encoding of xml special chars in test filename (issue 61) 4 | * Fixed remote code execution vulnerability 5 | * Updated package dependencies 6 | * Updated integrated phpmd.phar to version 2.9.1 7 | 8 | # Version 1.2.0 9 | 10 | * Added variable replacement in phpmd command (workspaceFolder and os homedir) 11 | * Small spelling fixes 12 | * Updated package dependencies 13 | * Added trimming of diagnostic messages (issue 48) 14 | 15 | # Version 1.1.0 16 | 17 | * Added clearing of diagnostics on file close (issue #36) 18 | * Added enabled setting (issue #37) 19 | 20 | # Version 1.0.4 21 | 22 | * Fix issue #30 23 | 24 | # Version 1.0.3 25 | 26 | * Fix issue #23 27 | 28 | # Version 1.0.2 29 | 30 | * Fix issue #18 31 | * Fix issue #19 32 | 33 | # Version 1.0.1 34 | 35 | * Fix issue #12 36 | * Fix issue #14 37 | 38 | # Version 1.0.0 39 | 40 | * Added jsdoc comments to all "things" 41 | * Improved logging 42 | * Added a changelog 43 | * Added a licence 44 | * Completed the README 45 | * Submitted to the VSCode marketplace, yay! 46 | 47 | # Version 0.5.0 48 | 49 | * Ship PHP mess detector PHAR with extension 50 | 51 | # Version 0.4.0 52 | 53 | * Added merging of defaults and user settings 54 | * Added PHP mess detector availability checks 55 | * Added PHP mess detector config file support 56 | 57 | # Version 0.3.0 58 | 59 | * Added logging to client console 60 | * Client-server connection setup refactoring 61 | 62 | # Version 0.2.0 63 | 64 | * Added unit tests 65 | 66 | # Version 0.1.0 67 | 68 | * Initial version with PHP mess detector functionality 69 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sandhjé Bouw 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode PHP Mess Detector 2 | 3 | [![Build Status](https://travis-ci.org/sandhje/vscode-phpmd.svg?branch=master)](https://travis-ci.org/sandhje/vscode-phpmd) 4 | [![codecov](https://codecov.io/gh/sandhje/vscode-phpmd/branch/master/graph/badge.svg)](https://codecov.io/gh/sandhje/vscode-phpmd) 5 | [![Join the chat at https://gitter.im/sandhje/vscode-phpmd](https://badges.gitter.im/sandhje/vscode-phpmd.svg)](https://gitter.im/sandhje/vscode-phpmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | ## Features 8 | 9 | * Analyze your PHP source code on save with PHP mess detector 10 | * No additional setup required if PHP is installed on your system 11 | * Customize which PHPMD rules are used 12 | * Supports custom PHPMD ruleset files 13 | 14 | ## Installation 15 | 16 | To install the extension: Press `F1`, type `ext install vscode-phpmd` 17 | 18 | ### Using the built-in PHPMD PHAR 19 | 20 | To use the built-in PHP mess detector you need to have PHP in your PATH. To test, open a shell or command window and type `php -v`, this should return "PHP x.x.x ...". If PHP is in your PATH you can directly use this extension, no further setup is required. 21 | 22 | ### Using a custom PHPMD PHAR or executable 23 | 24 | If you want to customize the default PHP mess detector command, e.g. you have PHP mess detector globally installed through composer or have PHP available on a different location, you can customize the command with the `command` setting. 25 | 26 | ## Configuration 27 | 28 | The following configuration options are available: 29 | 30 | ### phpmd.command: 31 | Customize the PHP mess detector command. If left empty the built-in PHPMD PHAR archive will be executed and PHP needs to be available on your PATH. If you want to use a different PHPMD PHAR you can customize the command here. 32 | 33 | #### Examples: 34 | To use PHPMD installed globally with composer on a windows machine set this setting to: 35 | ``` 36 | "phpmd.command": "C:/Users/{USER}/AppData/Roaming/Composer/vendor/bin/phpmd.bat" 37 | ``` 38 | 39 | Or to use your own PHPMD PHAR on a custom location: 40 | ``` 41 | "phpmd.command": "php C:/path/to/phpmd.phar"` 42 | ``` 43 | ***Security fix in version 1.3.0:*** *Before version 1.3.0 it was possible to set `phpmd.command` through workspace settings. This unfortunately opened possibilities for a remote code execution attack. As such, since version 1.3.0, this setting is disabled at workspace level to address this issue. I do understand though that some users used this workspace setting in their day to day work therefore it is still possible to override the phpmd.command setting through workspace settings via the phpmd.unsafeCommand setting. This setting is disabled by default, use at your own risk, see the explanation at the phpmd.unsafeCommandEnabled setting.* 44 | 45 | ### phpmd.unsafeCommand: 46 | Customize the PHP mess detector command from workspace settings. This setting is ignored by default if `phpmd.unsafeCommandEnabled` is not set to `true`. 47 | 48 | *Warning:* Please be aware that using this setting opens a possibility for a remote code execution attack. See the explanation at the `phpmd.unsafeCommandEnabled` setting. 49 | 50 | ### phpmd.unsafeCommandEnabled: 51 | Enable customizing the PHP mess detector command from workspace settings. Default is `false`. This setting can't be set in workspace settings. 52 | 53 | *Warning:* Please be aware that enabling this setting opens a possibility for a remote code execution attack. When opening a folder from an untrusted source, the opened folder could have a .vscode folder with a settings.json file defining the `phpmd.unsafeCommand` setting. The command configured there will be executed as soon as a php file is opened in the workspace. The author of the untrusted code could execute any command on your system through this setting, resulting in your system possibly being compromised. Use at your own risk. 54 | 55 | ### phpmd.rules: 56 | 57 | Customize the PHPMD ruleset files used. This option can also take the path to a custom [PHPMD ruleset file](https://phpmd.org/documentation/creating-a-ruleset.html). Use VS Code's workspace settings to control the rules or ruleset files per workspace. When setting a path to a ruleset file, and the path starts with "~/" this will be replaced with the OS homedir. The string "${workspaceFolder}" in a path to a ruleset file will be replaced with an absolute path to the folder in the workspace which relates to the file that is being validated. Refer to [PHPMD's documentation](https://phpmd.org/documentation/index.html) for more information on the ruleset parameter. 58 | 59 | #### Examples: 60 | To use only the cleancode ruleset and skip all the others: 61 | ``` 62 | "phpmd.rules": "cleancode" 63 | ``` 64 | 65 | Pass a comma seperated list of rulesets: 66 | ``` 67 | "phpmd.rules": "cleancode,codesize" 68 | ``` 69 | 70 | Pass the path to a ruleset file: 71 | ``` 72 | "phpmd.rules": "C:/path/to/phpmd_config.xml" 73 | ``` 74 | 75 | Pass the path to a ruleset file located in the home directory: 76 | ``` 77 | "phpmd.rules": "~/phpmd_config.xml" 78 | ``` 79 | 80 | Pass the path to a ruleset file located in the workspace folder: 81 | ``` 82 | "phpmd.rules": "${workspaceFolder}/phpmd_config.xml" 83 | ``` 84 | 85 | ### phpmd.verbose: 86 | Turn verbose logging on or off. All log entries can be viewed VS Code's output panel. Generally this can be turned off (default) unless you need to troubleshoot problems. 87 | 88 | #### Examples: 89 | To enable verbose logging: 90 | ``` 91 | "phpmd.verbose": true 92 | ``` 93 | 94 | ## System requirements 95 | * PHP_Depend >= 2.0.0 96 | * PHP >= 5.3.9 97 | * [PHP XML extension](https://www.php.net/manual/en/simplexml.installation.php) 98 | 99 | ## Troubleshooting 100 | * Turn on verbose logging through settings and check output 101 | * Ask a question on [gitter](https://gitter.im/sandhje/vscode-phpmd) 102 | * Found a bug? [file an issue](https://github.com/sandhje/vscode-phpmd/issues) (include the logs) 103 | 104 | ## Contributing 105 | 106 | If you found a bug or can help with adding a new feature to this extension you can submit any code through a pull request. The requirements for a pull request to be accepted are: 107 | 108 | * Add unit tests for all new code (code coverage must not drop) 109 | * Add JSDoc comments to all "things" 110 | * Make sure there are no TSLint violations (see tslint.json) 111 | 112 | Before contributing also make sure you are familiar with VSCode's [language server development](https://code.visualstudio.com/docs/extensions/example-language-server) 113 | 114 | Install all dependencies with [yarn](https://yarnpkg.com/lang/en/) 115 | 116 | ## History 117 | 118 | See client/CHANGELOG.md 119 | 120 | ## Acknowledgements 121 | 122 | * The people behind [PHPMD](https://phpmd.org/people-behind.html) 123 | * The Microsoft VSCode team for [VSCode](https://code.visualstudio.com/) and [vscode-languageserver-node](https://github.com/Microsoft/vscode-languageserver-node). 124 | * Quentin Dreyer for his OS homedir replacement solution (https://github.com/qkdreyer) 125 | * Shane Smith for his spelling fixes (https://github.com/shane-smith) 126 | * RyotaK for reporting the phpmd.command security issue (https://twitter.com/ryotkak) 127 | -------------------------------------------------------------------------------- /client/client.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as path from 'path'; 4 | import { workspace, ExtensionContext } from 'vscode'; 5 | import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind } from 'vscode-languageclient'; 6 | 7 | export function activate(context: ExtensionContext) { 8 | 9 | let serverModule = context.asAbsolutePath(path.join('out', 'server', 'init.js')); 10 | 11 | // Server debug options 12 | let debugOptions = { execArgv: ["--nolazy", "--inspect"] }; 13 | 14 | // Server options 15 | let serverOptions: ServerOptions = { 16 | run : { module: serverModule, transport: TransportKind.ipc }, 17 | debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } 18 | } 19 | 20 | // Client options 21 | let clientOptions: LanguageClientOptions = { 22 | documentSelector: ['php'], 23 | synchronize: { 24 | configurationSection: 'phpmd' 25 | } 26 | } 27 | 28 | // Client launched in debug mode? 29 | let forceDebug = process.execArgv.some(value => value.indexOf("--inspect") >= 0); 30 | 31 | // Create and start the client 32 | let client = new LanguageClient('vscode-phpmd', 'PHP Mess Detector', serverOptions, clientOptions, forceDebug); 33 | let disposable = client.start(); 34 | 35 | console.log("PHP Mess Detector server started"); 36 | 37 | // Push the disposable to the context's subscriptions so that the 38 | // client can be deactivated on extension deactivation 39 | context.subscriptions.push(disposable); 40 | } 41 | -------------------------------------------------------------------------------- /images/vscode-phpmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandhje/vscode-phpmd/3be2744556746cd432e2a8015a6c1635813617b6/images/vscode-phpmd.png -------------------------------------------------------------------------------- /images/vscode-phpmd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 61 | 66 | 69 | 70 | 73 | 74 | 77 | 78 | 81 | 82 | 85 | 86 | 89 | 90 | 93 | 94 | 97 | 98 | 101 | 102 | 105 | 106 | 109 | 110 | 113 | 114 | 117 | 118 | 121 | 122 | 125 | 126 | 127 | 128 | 133 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --full-trace 4 | --bail 5 | --inspect 6 | --colors 7 | tests/**/*.ts -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-phpmd", 3 | "displayName": "PHP Mess Detector", 4 | "description": "VSCode plugin for PHP Mess Detector", 5 | "author": "Ecodes.io", 6 | "license": "MIT", 7 | "version": "1.3.0", 8 | "publisher": "ecodes", 9 | "engines": { 10 | "vscode": "^1.37.0" 11 | }, 12 | "categories": [ 13 | "Linters" 14 | ], 15 | "activationEvents": [ 16 | "onLanguage:php" 17 | ], 18 | "keywords": [ 19 | "php", 20 | "phpmd", 21 | "mess detector", 22 | "diagnostics", 23 | "linter" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/sandhje/vscode-phpmd/issues" 27 | }, 28 | "homepage": "https://github.com/sandhje/vscode-phpmd", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/sandhje/vscode-phpmd.git" 32 | }, 33 | "icon": "images/vscode-phpmd.png", 34 | "main": "./out/client/client", 35 | "contributes": { 36 | "configuration": { 37 | "type": "object", 38 | "title": "PHP Mess Detector configuration", 39 | "properties": { 40 | "phpmd.enabled": { 41 | "type": "boolean", 42 | "default": true, 43 | "description": "Enable mess detector" 44 | }, 45 | "phpmd.command": { 46 | "type": "string", 47 | "scope": "machine", 48 | "default": "", 49 | "description": "The phpmd command. Leave empty to use the shipped phpmd phar (local php executable is required)" 50 | }, 51 | "phpmd.unsafeCommandEnabled": { 52 | "type": "boolean", 53 | "scope": "machine", 54 | "default": false, 55 | "description": "Enable overriding phpmd command from workspace. This potentially makes you vulnerable to a remote code execution attack, only enable this if you know what you're doing." 56 | }, 57 | "phpmd.unsafeCommand": { 58 | "type": "string", 59 | "scope": "machine-overridable", 60 | "default": "", 61 | "description": "Override phpmd command from workspace. This potentially makes you vulnerable to a remote code execution attack, only use this if you know what you're doing." 62 | }, 63 | "phpmd.rules": { 64 | "type": "string", 65 | "default": "cleancode,codesize,controversial,design,unusedcode,naming", 66 | "description": "Phpmd ruleset (comma separated list of rulesets or the location of a config file)" 67 | }, 68 | "phpmd.verbose": { 69 | "type": "boolean", 70 | "default": false, 71 | "description": "Enable verbose logging" 72 | }, 73 | "phpmd.clearOnClose": { 74 | "type": "boolean", 75 | "default": true, 76 | "description": "Clear diagnostics for a file upon closing it" 77 | } 78 | } 79 | } 80 | }, 81 | "scripts": { 82 | "vscode:prepublish": "tsc -p ./", 83 | "test": "nyc mocha --opts mocha.opts", 84 | "report-coverage": "nyc report --reporter=json", 85 | "compile": "tsc -p .", 86 | "watch": "tsc --watch -p ." 87 | }, 88 | "devDependencies": { 89 | "@types/chai": "^4.2.5", 90 | "@types/mocha": "^7.0.0", 91 | "@types/node": "^12.12.11", 92 | "@types/sinon": "^7.5.1", 93 | "@types/xml2js": "^0.4.5", 94 | "@types/vscode": "^1.37.0", 95 | "@testdeck/mocha": "^0.1.2", 96 | "chai": "^4.2.0", 97 | "codecov": "^3.8.1", 98 | "mocha": "^7.0.0", 99 | "nyc": "^15.0.0", 100 | "sinon": "^7.5.0", 101 | "source-map-support": "^0.5.16", 102 | "ts-node": "^8.5.2", 103 | "tslint": "^5.20.1", 104 | "typescript": "3.7.2" 105 | }, 106 | "dependencies": { 107 | "@open-sourcerers/j-stillery": "^0.2.0", 108 | "vscode-languageclient": "^5.2.1", 109 | "vscode-languageserver": "^5.2.1", 110 | "vscode-uri": "^2.1.1", 111 | "xml2js": "^0.4.17" 112 | }, 113 | "nyc": { 114 | "include": [ 115 | "server/*.ts", 116 | "server/**/*.ts" 117 | ], 118 | "extension": [ 119 | ".ts" 120 | ], 121 | "require": [ 122 | "ts-node/register" 123 | ], 124 | "reporter": [ 125 | "text-summary" 126 | ], 127 | "sourceMap": true, 128 | "instrument": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /phpmd/phpmd.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sandhje/vscode-phpmd/3be2744556746cd432e2a8015a6c1635813617b6/phpmd/phpmd.phar -------------------------------------------------------------------------------- /server/Server.ts: -------------------------------------------------------------------------------- 1 | import * as Path from "path"; 2 | import { 3 | createConnection, IConnection, InitializeResult, IPCMessageReader, IPCMessageWriter, 4 | TextDocument, TextDocumentIdentifier, TextDocuments, WorkspaceFolder 5 | } from "vscode-languageserver"; 6 | import PhpmdController from "./controller/PhpmdController"; 7 | import ClientConnectionNotifierFactory from "./factory/ClientConnectionNotifierFactory"; 8 | import ILoggerFactory from "./factory/ILoggerFactory"; 9 | import INotifierFactory from "./factory/INotifierFactory"; 10 | import PhpmdControllerFactory from "./factory/PhpmdControllerFactory"; 11 | import RemoteConsoleLoggerFactory from "./factory/RemoteConsoleLoggerFactory"; 12 | import IPhpmdSettingsModel from "./model/IPhpmdSettingsModel"; 13 | import ILogger from "./service/logger/ILogger"; 14 | import NullLogger from "./service/logger/NullLogger"; 15 | import INotifier from "./service/notifier/INotifier"; 16 | import NullNotifier from "./service/notifier/NullNotifier"; 17 | import IPhpmdEnvironmentModel from "./model/IPhpmdEnvironmentModel"; 18 | import { homedir } from "os"; 19 | 20 | /** 21 | * PHP mess detector language server 22 | * 23 | * @module vscode-phpmd/server 24 | * @author Sandhjé Bouw (sandhje@ecodes.io) 25 | */ 26 | class Server { 27 | /** 28 | * VSCode server connection class 29 | * 30 | * @property {IConnection} connection 31 | */ 32 | private connection: IConnection; 33 | 34 | /** 35 | * VSCode client workspace folders 36 | * 37 | * @property {WorkspaceFolder[]} workspaceFolders 38 | */ 39 | private workspaceFolders: WorkspaceFolder[]; 40 | 41 | /** 42 | * OS home directory 43 | * 44 | * @property {string} homeDir 45 | */ 46 | private homeDir: string; 47 | 48 | /** 49 | * PHP mess detector controller class 50 | * 51 | * Contains the main API to run the PHPMD command and process its results. 52 | * 53 | * @property {PhpmdController} PhpmdController 54 | */ 55 | private controller: PhpmdController; 56 | 57 | /** 58 | * PHP mess detector controller factory class 59 | * 60 | * Creates PHPMD controller instances. 61 | * 62 | * @property {PhpmdControllerFactory} controllerFactory 63 | */ 64 | private controllerFactory: PhpmdControllerFactory; 65 | 66 | /** 67 | * VSCode document manager class 68 | * 69 | * API to access and manipulate documents open in the connected VSCode window. 70 | * 71 | * @property {TextDocuments} documentsManager 72 | */ 73 | private documentsManager: TextDocuments; 74 | 75 | /** 76 | * Logger class 77 | * 78 | * Process log messages of various severity levels. 79 | * 80 | * @property {ILogger} logger 81 | */ 82 | private logger: ILogger; 83 | 84 | /** 85 | * Logger factory class 86 | * 87 | * Creates logger instances. 88 | * 89 | * @property {ILoggerFactory} loggerFactory 90 | */ 91 | private loggerFactory: ILoggerFactory; 92 | 93 | /** 94 | * Notifier class 95 | * 96 | * Send end-user friendly notifications. 97 | * 98 | * @property {INotifier} notifier 99 | */ 100 | private notifier: INotifier; 101 | 102 | /** 103 | * Notifier factory class 104 | * 105 | * Creates notifier instances. 106 | * 107 | * @property {INotifierFactory} notifierFactory 108 | */ 109 | private notifierFactory: INotifierFactory; 110 | 111 | /** 112 | * Connection setter 113 | * 114 | * Allows injection of connection for better testability. 115 | * 116 | * @param {IConnection} connection 117 | * @returns {void} 118 | */ 119 | public setConnection(connection: IConnection): void { 120 | this.connection = connection; 121 | } 122 | 123 | /** 124 | * WorkspaceFolders setter 125 | * 126 | * Allows injection of workspaceFolders for better testability. 127 | * 128 | * @param {WorkspaceFolder[]} workspaceFolders 129 | * @returns {void} 130 | */ 131 | public setWorkspaceFolders(workspaceFolders: WorkspaceFolder[]): void { 132 | this.workspaceFolders = workspaceFolders; 133 | } 134 | 135 | /** 136 | * HomeDir setter 137 | * 138 | * Allows injection of homedir for better testability. 139 | * 140 | * @param {string} HomeDir 141 | * @returns {void} 142 | */ 143 | public setHomeDir(homeDir: string): void { 144 | this.homeDir = homeDir; 145 | } 146 | 147 | /** 148 | * ControllerFactory setter 149 | * 150 | * Allows injection of controllerFactory for better testability. 151 | * 152 | * @param {PhpmdControllerFactory} controllerFactory 153 | * @returns {void} 154 | */ 155 | public setControllerFactory(controllerFactory: PhpmdControllerFactory): void { 156 | this.controllerFactory = controllerFactory; 157 | } 158 | 159 | /** 160 | * LoggerFactory setter 161 | * 162 | * Allows injection of loggerFactory for better testability. 163 | * 164 | * @param {ILoggerFactory} loggerFactory 165 | * @returns {void} 166 | */ 167 | public setLoggerFactory(loggerFactory: ILoggerFactory): void { 168 | this.loggerFactory = loggerFactory; 169 | } 170 | 171 | /** 172 | * NotifierFactory setter 173 | * 174 | * Allows injection of notifierFactory for better testability. 175 | * 176 | * @param {INotifierFactory} notifierFactory 177 | * @returns {void} 178 | */ 179 | public setNotifierFactory(notifierFactory: INotifierFactory): void { 180 | this.notifierFactory = notifierFactory; 181 | } 182 | 183 | /** 184 | * DocumentsManager setter 185 | * 186 | * Allows injection of documentsManager for better testability. 187 | * 188 | * @param {TextDocuments} documentsManager 189 | * @returns {void} 190 | */ 191 | public setDocumentsManager(documentsManager: TextDocuments): void { 192 | this.documentsManager = documentsManager; 193 | } 194 | 195 | /** 196 | * Controller setter 197 | * 198 | * Allows injection of controller for better testability. 199 | * 200 | * @param {PhpmdController} controller 201 | * @returns {void} 202 | */ 203 | public setController(controller: PhpmdController): void { 204 | this.controller = controller; 205 | } 206 | 207 | /** 208 | * Logger setter 209 | * 210 | * Allows injection of logger for better testability. 211 | * 212 | * @param {ILogger} logger 213 | * @returns {void} 214 | */ 215 | public setLogger(logger: ILogger): void { 216 | this.logger = logger; 217 | } 218 | 219 | /** 220 | * Notifier setter 221 | * 222 | * Allows injection of notifier for better testability. 223 | * 224 | * @param {INotifier} notifier 225 | * @returns {void} 226 | */ 227 | public setNotifier(notifier: INotifier): void { 228 | this.notifier = notifier; 229 | } 230 | 231 | /** 232 | * Server's main point of entry 233 | * 234 | * The main method sets up the connection starts the listening and registers relevant event listeners and 235 | * their handlers on the connection. 236 | * 237 | * @returns {void} 238 | */ 239 | public main(): void { 240 | // Get VSCode documentManager and connection 241 | let documentsManager: TextDocuments = this.getDocumentsManager(); 242 | let connection: IConnection = this.getConnection(); 243 | 244 | // Create logger 245 | this.createLogger(connection); 246 | 247 | // Create notifier 248 | this.createNotifier(connection); 249 | 250 | // Manage documents for connection 251 | documentsManager.listen(connection); 252 | 253 | // The settings have changed. Is send on server activation as well. 254 | connection.onDidChangeConfiguration((change) => { 255 | this.getLogger().info("Configuration change triggerd, validating all open documents."); 256 | 257 | let settings = this.createSettings(change.settings.phpmd); 258 | let environment = this.createEnvironment(); 259 | this.logger.setVerbose(settings.verbose); 260 | 261 | this.getLogger().info("Creating controller", true); 262 | this.createController(connection, settings, environment); 263 | 264 | // (Re)Validate any open text documents 265 | documentsManager.all().forEach((document: TextDocument) => { 266 | this.getLogger().info("Validating document " + document.uri, true); 267 | this.getController().validate(document).then((result: boolean) => { 268 | this.getLogger().info("Document validation after config change completed successfully"); 269 | }, (err: Error) => { 270 | this.getLogger().error( 271 | "An error occured during document validation after config change with the following message: " 272 | + err.message 273 | ); 274 | }); 275 | }); 276 | }); 277 | 278 | // A php document was opened 279 | connection.onDidOpenTextDocument((parameters) => { 280 | this.getLogger().info("New document opened, starting validation."); 281 | 282 | let document: TextDocumentIdentifier = parameters.textDocument; 283 | 284 | this.getController().validate(document).then((result: boolean) => { 285 | this.getLogger().info("Document validation after open completed successfully"); 286 | }, (err: Error) => { 287 | this.getLogger().error( 288 | "An error occured during document validation after open with the following message: " + err.message 289 | ); 290 | }); 291 | }); 292 | 293 | // A php document was saved 294 | connection.onDidSaveTextDocument((parameters) => { 295 | this.getLogger().info("Document saved, starting validation."); 296 | 297 | let document: TextDocumentIdentifier = parameters.textDocument; 298 | 299 | this.getController().validate(document).then((result: boolean) => { 300 | this.getLogger().info("Document validation after save completed successfully"); 301 | }, (err: Error) => { 302 | this.getLogger().error( 303 | "An error occured during document validation after save with the following message: " + err.message 304 | ); 305 | }); 306 | }); 307 | 308 | // A php document was closed 309 | connection.onDidCloseTextDocument((parameters) => { 310 | this.getLogger().info("Document closed, clearing messages."); 311 | 312 | let document: TextDocumentIdentifier = parameters.textDocument; 313 | 314 | this.getController().clear(document); 315 | }); 316 | 317 | // Set connection capabilities 318 | connection.onInitialize((params) => { 319 | this.getLogger().info("Language server connection initialized."); 320 | 321 | if (params && params.workspaceFolders) { 322 | this.getLogger().info(`Setting workspaceFolders ${JSON.stringify(params.workspaceFolders)}.`); 323 | this.setWorkspaceFolders(params.workspaceFolders) 324 | } 325 | 326 | return this.getInitializeResult(); 327 | }); 328 | 329 | // Listen on the connection 330 | connection.listen(); 331 | } 332 | 333 | /** 334 | * Return the initializeResult object for the server's documentManager 335 | * 336 | * @returns {InitializeResult} 337 | */ 338 | protected getInitializeResult(): InitializeResult { 339 | return { 340 | capabilities: { 341 | textDocumentSync: this.getDocumentsManager().syncKind, 342 | } 343 | }; 344 | } 345 | 346 | /** 347 | * Get the VSCode connection or create one if no connection was set before 348 | * 349 | * @returns {IConnection} 350 | */ 351 | protected getConnection(): IConnection { 352 | if (!this.connection) { 353 | this.connection = createConnection(new IPCMessageReader(process), new IPCMessageWriter(process)); 354 | } 355 | 356 | return this.connection; 357 | } 358 | 359 | /** 360 | * Get the VSCode connection workspaceFolders 361 | * 362 | * @returns {WorkspaceFolder[]} 363 | */ 364 | protected getWorkspaceFolders(): WorkspaceFolder[] { 365 | return this.workspaceFolders; 366 | } 367 | 368 | /** 369 | * Get the OS HomeDir 370 | * 371 | * @returns {string} 372 | */ 373 | protected getHomeDir(): string { 374 | if (!this.homeDir) { 375 | this.homeDir = homedir(); 376 | } 377 | 378 | return this.homeDir; 379 | } 380 | 381 | /** 382 | * Get the VSCode documentsManager or create on if no documentsManager was set before 383 | * 384 | * @returns {TextDocuments} 385 | */ 386 | protected getDocumentsManager(): TextDocuments { 387 | if (!this.documentsManager) { 388 | this.documentsManager = new TextDocuments(); 389 | } 390 | 391 | return this.documentsManager; 392 | } 393 | 394 | /** 395 | * Get the controller factory or create a PhpmdControllerFactory if no controller factory was set before 396 | * 397 | * @returns {PhpmdControllerFactory} 398 | */ 399 | protected getControllerFactory(): PhpmdControllerFactory { 400 | if (!this.controllerFactory) { 401 | this.controllerFactory = new PhpmdControllerFactory(); 402 | } 403 | 404 | return this.controllerFactory; 405 | } 406 | 407 | /** 408 | * Get the logger factory or create a RemoteConsoleLoggerFactory if no logger factory was set before 409 | * 410 | * @returns {ILoggerFactory} 411 | */ 412 | protected getLoggerFactory(): ILoggerFactory { 413 | if (!this.loggerFactory) { 414 | this.loggerFactory = new RemoteConsoleLoggerFactory(); 415 | } 416 | 417 | return this.loggerFactory; 418 | } 419 | 420 | /** 421 | * Get the notifier factory or create a ClientConnectionNotifierFactory if no notifier factory was set before 422 | * 423 | * @returns {INotifierFactory} 424 | */ 425 | protected getNotifierFactory(): INotifierFactory { 426 | if (!this.notifierFactory) { 427 | this.notifierFactory = new ClientConnectionNotifierFactory(); 428 | } 429 | 430 | return this.notifierFactory; 431 | } 432 | 433 | /** 434 | * Create a settings model 435 | * 436 | * Create a PHPMD settings model from the values send through the VSCode connection with some sane default values. 437 | * 438 | * @param {IPhpmdSettingsModel} 439 | */ 440 | protected createSettings(values: any): IPhpmdSettingsModel { 441 | let defaults: IPhpmdSettingsModel = { 442 | enabled: true, 443 | command: "", 444 | unsafeCommandEnabled: false, 445 | unsafeCommand: "", 446 | rules: "", 447 | verbose: false, 448 | clearOnClose: true 449 | }; 450 | 451 | let settings: IPhpmdSettingsModel = Object.assign(defaults, values); 452 | 453 | if (!settings.command) { 454 | settings.command = this.getDefaultCommand(); 455 | } 456 | 457 | return settings; 458 | } 459 | 460 | /** 461 | * Create a environment model 462 | * 463 | * Create a PHPMD environment model from the server environment 464 | * 465 | * @param {IPhpmdEnvironmentModel} 466 | */ 467 | protected createEnvironment(): IPhpmdEnvironmentModel { 468 | const environment: IPhpmdEnvironmentModel = { 469 | workspaceFolders: this.getWorkspaceFolders(), 470 | homeDir: this.getHomeDir() 471 | }; 472 | 473 | return environment; 474 | } 475 | 476 | /** 477 | * Get the default command 478 | * 479 | * Get the default PHPMD command string to execute the shipped PHPMD phar file with php 480 | * 481 | * @returns {string} 482 | */ 483 | protected getDefaultCommand(): string { 484 | let serverPath = Path.dirname(process.argv[1]); 485 | let phpmdPath = Path.normalize(serverPath + "/../../phpmd/phpmd.phar"); 486 | let executable = "php " + phpmdPath; 487 | 488 | return executable; 489 | } 490 | 491 | /** 492 | * Create the server's PHPMD controller 493 | * 494 | * Instantiates the controller by using this server's controller factory. Arranges the controller 495 | * to use the correct connection, settings, logger and notifier. Assigns the controller to the 496 | * controller property of this server instance. 497 | * 498 | * @param {IConnection} connection 499 | * @param {IPhpmdSettingsModel} settings 500 | * @returns {void} 501 | */ 502 | protected createController(connection: IConnection, settings: IPhpmdSettingsModel, environment: IPhpmdEnvironmentModel): void { 503 | let controllerFactory = this.getControllerFactory(); 504 | controllerFactory.setConnection(connection); 505 | controllerFactory.setSettings(settings); 506 | controllerFactory.setEnvironment(environment); 507 | 508 | this.controller = controllerFactory.create(); 509 | this.controller.setLogger(this.getLogger()); 510 | this.controller.setNotifier(this.getNotifier()); 511 | 512 | this.getLogger().info(`Created controller with settings '${JSON.stringify(settings)}' and environment '${JSON.stringify(environment)}'`); 513 | } 514 | 515 | /** 516 | * Create the server's logger 517 | * 518 | * Instantiates the logger by using this server's logger factory. Arranges the logger to use the 519 | * correct connection. Assigns the logger to the logger property of this server instance. 520 | * 521 | * @param {IConnection} connection 522 | * @returns {void} 523 | */ 524 | protected createLogger(connection: IConnection): void { 525 | let loggerFactory = this.getLoggerFactory(); 526 | loggerFactory.setConnection(connection); 527 | 528 | this.logger = loggerFactory.create(); 529 | } 530 | 531 | /** 532 | * Create the server's notifier 533 | * 534 | * Instantiates the notifier by using this server's notifier factory. Arranges the notifier to use the 535 | * correct connection. Assigns the notifier to the notifier property of this server instance. 536 | * 537 | * @param {IConnection} connection 538 | * @returns {void} 539 | */ 540 | protected createNotifier(connection: IConnection) { 541 | let notifierFactory = this.getNotifierFactory(); 542 | notifierFactory.setConnection(connection); 543 | 544 | this.notifier = notifierFactory.create(); 545 | } 546 | 547 | /** 548 | * Get the server's PHPMD controller 549 | * 550 | * Logs and throws an error if the controller was not created before calling this method. 551 | * 552 | * @throws {Error} 553 | * @returns {PhpmdController} 554 | */ 555 | protected getController(): PhpmdController { 556 | if (!this.controller) { 557 | this.getLogger().error("Controller not initialized. Aborting"); 558 | throw Error("Controller not initialized. Aborting."); 559 | } 560 | 561 | return this.controller; 562 | } 563 | 564 | /** 565 | * Get the server's logger 566 | * 567 | * Returns a null object implementing the ILogger interface if no logger was created before calling this method. 568 | * 569 | * @returns {ILogger} 570 | */ 571 | protected getLogger(): ILogger { 572 | if (!this.logger) { 573 | return new NullLogger(); 574 | } 575 | 576 | return this.logger; 577 | } 578 | 579 | /** 580 | * Get the server's notifier 581 | * 582 | * Returns a null object implementing the INotifier interface if no notifier was created before calling this method. 583 | * 584 | * @returns {INotifier} 585 | */ 586 | protected getNotifier(): INotifier { 587 | if (!this.notifier) { 588 | return new NullNotifier(); 589 | } 590 | 591 | return this.notifier; 592 | } 593 | } 594 | 595 | export default Server; 596 | -------------------------------------------------------------------------------- /server/controller/PhpmdController.ts: -------------------------------------------------------------------------------- 1 | import { Pipeline } from "@open-sourcerers/j-stillery"; 2 | import { 3 | CompletionItem, CompletionItemKind, createConnection, Diagnostic, 4 | DiagnosticSeverity, IConnection, InitializeParams, InitializeResult, 5 | IPCMessageReader, IPCMessageWriter, TextDocument, TextDocumentIdentifier, 6 | TextDocumentPositionParams, TextDocuments, TextDocumentSyncKind 7 | } from "vscode-languageserver"; 8 | import PipelineFactory from "../factory/PipelineFactory"; 9 | import PipelinePayloadFactory from "../factory/PipelinePayloadFactory"; 10 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 11 | import PipelineErrorModel from "../model/PipelineErrorModel"; 12 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 13 | import ILogger from "../service/logger/ILogger"; 14 | import NullLogger from "../service/logger/NullLogger"; 15 | import INotifier from "../service/notifier/INotifier"; 16 | import NullNotifier from "../service/notifier/NullNotifier"; 17 | import PhpmdService from "../service/PhpmdService"; 18 | import PhpmdCommandBuilder from "../service/PhpmdCommandBuilder"; 19 | import IPhpmdEnvironmentModel from "../model/IPhpmdEnvironmentModel"; 20 | 21 | /** 22 | * PHP mess detector controller 23 | * 24 | * Defines actions available to the language server 25 | * 26 | * @module vscode-phpmd/server/controller 27 | * @author Sandhjé Bouw (sandhje@ecodes.io) 28 | */ 29 | class PhpmdController { 30 | /** 31 | * Validation pipeline 32 | * 33 | * @property {Pipeline; 36 | 37 | /** 38 | * Factory class for the pipelinePayload model 39 | * 40 | * @property {PipelinePayloadFactory} pipelinePayloadFactory 41 | */ 42 | private pipelinePayloadFactory: PipelinePayloadFactory; 43 | 44 | /** 45 | * Logger class 46 | * 47 | * Process log messages of various severity levels. 48 | * 49 | * @property {ILogger} logger 50 | */ 51 | private logger: ILogger; 52 | 53 | /** 54 | * Notifier class 55 | * 56 | * Send end-user friendly notifications. 57 | * 58 | * @property {INotifier} notifier 59 | */ 60 | private notifier: INotifier; 61 | 62 | /** 63 | * PHP mess detector command builder 64 | * 65 | * Service class used to build the phpmd command 66 | * 67 | * @property {PhpmdCommandBuilder} commandBuilder 68 | */ 69 | private commandBuilder: PhpmdCommandBuilder; 70 | 71 | /** 72 | * PHP mess detector service 73 | * 74 | * Service class used to interact with the PHP mess detecter executable 75 | * 76 | * @property {PhpmdService} service 77 | */ 78 | private service: PhpmdService; 79 | 80 | /** 81 | * Executable test error counter 82 | * 83 | * Keeps track of the number of times the phpmd executable was tested and the test failed. 84 | * 85 | * @property {number} phpmdTestErrorCount 86 | */ 87 | private phpmdTestErrorCount: number = 0; 88 | 89 | /** 90 | * Create PHPMD controller instance with the passed connection and settings 91 | * 92 | * @param {IConnection} connection 93 | * @param {IPhpmdSettingsModel} settings 94 | */ 95 | constructor( 96 | private connection: IConnection, 97 | private settings: IPhpmdSettingsModel, 98 | private environment: IPhpmdEnvironmentModel 99 | ) { } 100 | 101 | /** 102 | * Validate the passed document with PHP mess detector 103 | * 104 | * @param {TextDocument|TextDocumentIdentifier} document 105 | * @returns {Promise} Resolves with true on success, rejects with error on failure 106 | */ 107 | public validate(document: TextDocument | TextDocumentIdentifier): Promise { 108 | if (!this.settings.enabled) { 109 | this.getLogger().info("Extension disabled, bailing out"); 110 | return Promise.resolve(true); 111 | } 112 | 113 | this.getLogger().info("PHP Mess Detector validation started for " + document.uri, true); 114 | 115 | return new Promise((resolve, reject) => { 116 | // Test version 117 | this.getService().testPhpmd().then((data: boolean) => { 118 | let payload = this.getPipelinePayloadFactory().setUri(document.uri).create(); 119 | 120 | this.getPipeline().run(payload).then((output) => { 121 | let diagnostics = output.diagnostics; 122 | 123 | // Send the computed diagnostics to VSCode. 124 | this.getLogger().info( 125 | "PHP Mess Detector validation completed for " + document.uri 126 | + ". " + diagnostics.length + " problems found", 127 | true 128 | ); 129 | this.connection.sendDiagnostics({uri: output.uri, diagnostics}); 130 | 131 | // Resolve the validate promise 132 | resolve(true); 133 | }, (err: any) => { 134 | // Don't notify the user of the error if silent is defined on the error and is truthy 135 | if (!err.silent) { 136 | this.getNotifier().error("An error occured while executing PHP Mess Detector"); 137 | } 138 | 139 | // If an error property is defined on the error reject the validate promise with that 140 | // value instead, this is the case for PipelineErrorModel instances 141 | if (err.error) { 142 | return reject(err.error); 143 | } 144 | 145 | // Reject the validate promise 146 | return reject(err); 147 | }); 148 | }, (err: Error) => { 149 | // Only notify client of "PHPMD test error" once per controller instance 150 | if (!this.phpmdTestErrorCount) { 151 | this.getNotifier().error("Unable to execute PHPMD command (" + this.settings.command + ")"); 152 | } 153 | 154 | this.phpmdTestErrorCount++; 155 | 156 | // Reject the validate promise 157 | reject(err); 158 | }); 159 | }); 160 | } 161 | 162 | /** 163 | * Clear diagnostics for the text documents 164 | * 165 | * @param document 166 | */ 167 | public clear(document: TextDocument | TextDocumentIdentifier): void { 168 | if (!this.settings.clearOnClose) { 169 | this.getLogger().info("Clearing of diagnostics disabled in settings"); 170 | return; 171 | } 172 | 173 | this.getLogger().info("Clearing diagnostics for " + document.uri); 174 | this.connection.sendDiagnostics({uri: document.uri, diagnostics: []}); 175 | } 176 | 177 | /** 178 | * PhpmdCommandBuilder setter 179 | * 180 | * Allows injection of phpmdCommandBuilder for better testability. 181 | * 182 | * @param {PhpmdCommandBuilder} commandBuilder 183 | * @returns {void} 184 | */ 185 | public setCommandBuilder(commandBuilder: PhpmdCommandBuilder): void { 186 | this.commandBuilder = commandBuilder; 187 | } 188 | 189 | /** 190 | * PhpmdService setter 191 | * 192 | * Allows injection of phpmdService for better testability. 193 | * 194 | * @param {PhpmdService} service 195 | * @returns {void} 196 | */ 197 | public setService(service: PhpmdService): void { 198 | this.service = service; 199 | } 200 | 201 | /** 202 | * Pipeline setter 203 | * 204 | * Allows injection of the validation pipeline for better testability. 205 | * 206 | * @param {Pipeline): void { 210 | this.pipeline = pipeline; 211 | } 212 | 213 | /** 214 | * PipelinePayload factory setter 215 | * 216 | * Allows injection of pipeline payload factory for better testability. 217 | * 218 | * @param {PipelinePayloadFactory} pipelinePayloadFactory 219 | * @returns {void} 220 | */ 221 | public setPipelinePayloadFactory(pipelinePayloadFactory: PipelinePayloadFactory): void { 222 | this.pipelinePayloadFactory = pipelinePayloadFactory; 223 | } 224 | 225 | /** 226 | * Logger setter 227 | * 228 | * Allows injection of the logger for better testability. 229 | * 230 | * @param {ILogger} logger 231 | * @returns {void} 232 | */ 233 | public setLogger(logger: ILogger): void { 234 | this.logger = logger; 235 | } 236 | 237 | /** 238 | * Notifier setter 239 | * 240 | * Allows injection of the notifier for better testability. 241 | * 242 | * @param {INotifier} notifier 243 | * @returns {void} 244 | */ 245 | public setNotifier(notifier: INotifier): void { 246 | this.notifier = notifier; 247 | } 248 | 249 | /** 250 | * Get the PHP mess detector command builder 251 | * 252 | * Create a command builder based on this server's settings if no service was set before 253 | * 254 | * @returns {PhpmdCommandBuilder} 255 | */ 256 | protected getCommandBuilder(): PhpmdCommandBuilder { 257 | if (!this.commandBuilder) { 258 | // TODO: add workspacefolders and homedir from settings 259 | this.commandBuilder = new PhpmdCommandBuilder( 260 | this.settings.command, 261 | this.settings.unsafeCommandEnabled, 262 | this.settings.unsafeCommand, 263 | this.environment.workspaceFolders, 264 | this.environment.homeDir 265 | ); 266 | this.commandBuilder.setLogger(this.getLogger()); 267 | } 268 | 269 | return this.commandBuilder; 270 | } 271 | 272 | /** 273 | * Get the PHP mess detector service 274 | * 275 | * Create a service based on this server's settings if no service was set before 276 | * 277 | * @returns {PhpmdService} 278 | */ 279 | protected getService(): PhpmdService { 280 | if (!this.service) { 281 | this.service = new PhpmdService(this.getCommandBuilder()); 282 | this.service.setLogger(this.getLogger()); 283 | } 284 | 285 | return this.service; 286 | } 287 | 288 | /** 289 | * Get the validation pipeline 290 | * 291 | * Create a pipline based on this server's settings if no pipeline was set before 292 | * 293 | * @returns {Pipeline} 294 | */ 295 | protected getPipeline(): Pipeline { 296 | if (!this.pipeline) { 297 | this.pipeline = new PipelineFactory(this.settings, this.environment, this.getLogger()).create(); 298 | } 299 | 300 | return this.pipeline; 301 | } 302 | 303 | /** 304 | * Get the pipeline payload factory 305 | * 306 | * Create a pipline payload factory if no factory was set before 307 | * 308 | * @returns {PipelinePayloadFactory} 309 | */ 310 | protected getPipelinePayloadFactory(): PipelinePayloadFactory { 311 | if (!this.pipelinePayloadFactory) { 312 | this.pipelinePayloadFactory = new PipelinePayloadFactory(""); 313 | } 314 | 315 | return this.pipelinePayloadFactory; 316 | } 317 | 318 | /** 319 | * Get the logger 320 | * 321 | * Create a null object implementing the ILogger interface if no logger was set before 322 | * 323 | * @returns {ILogger} 324 | */ 325 | protected getLogger(): ILogger { 326 | if (!this.logger) { 327 | this.logger = new NullLogger(); 328 | } 329 | 330 | return this.logger; 331 | } 332 | 333 | /** 334 | * Get the notifier 335 | * 336 | * Create a null object implementing the INotifier interface if no notifier was set before 337 | * 338 | * @returns {INotifier} 339 | */ 340 | protected getNotifier(): INotifier { 341 | if (!this.notifier) { 342 | this.notifier = new NullNotifier(); 343 | } 344 | 345 | return this.notifier; 346 | } 347 | } 348 | 349 | export default PhpmdController; 350 | -------------------------------------------------------------------------------- /server/factory/BuildDiagnosticsTaskFactory.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import * as Xml2Js from "xml2js"; 3 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 4 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 5 | import ILogger from "../service/logger/ILogger"; 6 | import NullLogger from "../service/logger/NullLogger"; 7 | import BuildDiagnosticsStrategy from "../service/pipeline/BuildDiagnosticsStrategy"; 8 | import IFactory from "./IFactory"; 9 | 10 | /** 11 | * BuildDiagnostics task factory 12 | * 13 | * @module vscode-phpmd/server/factory 14 | * @author Sandhjé Bouw (sandhje@ecodes.io) 15 | */ 16 | class BuildDiagnosticsTaskFactory implements IFactory> { 17 | /** 18 | * @param {IPhpmdSettingsModel} settings 19 | */ 20 | constructor( 21 | private settings: IPhpmdSettingsModel, 22 | private logger: ILogger = new NullLogger() 23 | ) { } 24 | 25 | /** 26 | * Create BuildDiagnostics task instance 27 | * 28 | * @see IFactory::create 29 | * @returns {Task} 30 | */ 31 | public create(): Task { 32 | let strategy = new BuildDiagnosticsStrategy(this.logger); 33 | 34 | return new Task(strategy); 35 | } 36 | } 37 | 38 | export default BuildDiagnosticsTaskFactory; 39 | -------------------------------------------------------------------------------- /server/factory/ClientConnectionNotifierFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import ClientConnectionNotifier from "../service/notifier/ClientConnectionNotifier"; 3 | import INotifier from "../service/notifier/INotifier"; 4 | import INotifierFactory from "./INotifierFactory"; 5 | 6 | /** 7 | * ClientConnection notifier factory 8 | * 9 | * @module vscode-phpmd/server/factory 10 | * @author Sandhjé Bouw (sandhje@ecodes.io) 11 | */ 12 | class ClientConnectionNotifierFactory implements INotifierFactory { 13 | /** 14 | * @property {IConnection} connection 15 | */ 16 | private connection: IConnection; 17 | 18 | /** 19 | * Create ClientConnection notifier instance 20 | * 21 | * Throw error if no connection was set. 22 | * 23 | * @see IFactory::create 24 | * @throws {Error} 25 | * @returns {INotifier} 26 | */ 27 | public create(): INotifier { 28 | if (!this.connection) { 29 | throw Error("Unable to create ClientConnectionNotifier, no connection set"); 30 | } 31 | 32 | return new ClientConnectionNotifier(this.connection); 33 | } 34 | 35 | /** 36 | * Set VSCode client connection 37 | * 38 | * @see INotifierFactory::setConnection 39 | * @param {IConnection} connection 40 | * @returns {void} 41 | */ 42 | public setConnection(connection: IConnection): void { 43 | this.connection = connection; 44 | } 45 | } 46 | 47 | export default ClientConnectionNotifierFactory; 48 | -------------------------------------------------------------------------------- /server/factory/ExecuteProcessTaskFactory.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 3 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 4 | import ILogger from "../service/logger/ILogger"; 5 | import NullLogger from "../service/logger/NullLogger"; 6 | import ExecuteProcessStrategy from "../service/pipeline/ExecuteProcessStrategy"; 7 | import IFactory from "./IFactory"; 8 | import PhpmdCommandBuilder from "../service/PhpmdCommandBuilder"; 9 | import IPhpmdEnvironmentModel from "../model/IPhpmdEnvironmentModel"; 10 | 11 | /** 12 | * ExecuteProcess task factory 13 | * 14 | * @module vscode-phpmd/server/factory 15 | * @author Sandhjé Bouw (sandhje@ecodes.io) 16 | */ 17 | class ExecuteProcessTaskFactory implements IFactory> { 18 | /** 19 | * PHP mess detector command builder 20 | * 21 | * Service class used to build the phpmd command 22 | * 23 | * @property {PhpmdCommandBuilder} commandBuilder 24 | */ 25 | private commandBuilder: PhpmdCommandBuilder; 26 | 27 | /** 28 | * @param {IPhpmdSettingsModel} settings 29 | * @param {ILogger} logger 30 | */ 31 | constructor( 32 | private settings: IPhpmdSettingsModel, 33 | private environment: IPhpmdEnvironmentModel, 34 | private logger: ILogger = new NullLogger() 35 | ) { } 36 | 37 | /** 38 | * PhpmdCommandBuilder setter 39 | * 40 | * Allows injection of phpmdCommandBuilder for better testability. 41 | * 42 | * @param {PhpmdCommandBuilder} commandBuilder 43 | * @returns {void} 44 | */ 45 | public setCommandBuilder(commandBuilder: PhpmdCommandBuilder): void { 46 | this.commandBuilder = commandBuilder; 47 | } 48 | 49 | /** 50 | * Get the PHP mess detector command builder 51 | * 52 | * Create a command builder based on this server's settings if no service was set before 53 | * 54 | * @returns {PhpmdCommandBuilder} 55 | */ 56 | protected getCommandBuilder(): PhpmdCommandBuilder { 57 | if (!this.commandBuilder) { 58 | // TODO: add workspacefolders and homedir from settings 59 | this.commandBuilder = new PhpmdCommandBuilder( 60 | this.settings.command, 61 | this.settings.unsafeCommandEnabled, 62 | this.settings.unsafeCommand, 63 | this.environment.workspaceFolders, 64 | this.environment.homeDir 65 | ); 66 | this.commandBuilder.setLogger(this.logger); 67 | } 68 | 69 | return this.commandBuilder; 70 | } 71 | 72 | /** 73 | * Create ExecuteProcess task instance 74 | * 75 | * @see IFactory::create 76 | * @returns {Task} 77 | */ 78 | public create(): Task { 79 | let strategy = new ExecuteProcessStrategy(this.getCommandBuilder(), this.settings.rules, this.logger); 80 | 81 | return new Task(strategy); 82 | } 83 | } 84 | 85 | export default ExecuteProcessTaskFactory; 86 | -------------------------------------------------------------------------------- /server/factory/IFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Factory interface 3 | * 4 | * @module vscode-phpmd/server/factory 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | interface IFactory { 8 | /** 9 | * Create and return an instance of T 10 | * 11 | * @returns {T} 12 | */ 13 | create: () => T; 14 | } 15 | 16 | export default IFactory; 17 | -------------------------------------------------------------------------------- /server/factory/ILoggerFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import ILogger from "../service/logger/ILogger"; 3 | import IFactory from "./IFactory"; 4 | 5 | /** 6 | * ILogger factory interface 7 | * 8 | * @module vscode-phpmd/server/factory 9 | * @author Sandhjé Bouw (sandhje@ecodes.io) 10 | */ 11 | interface ILoggerFactory extends IFactory { 12 | /** 13 | * Set logger connection 14 | * 15 | * @param {IConnection} connection 16 | * @returns {void} 17 | */ 18 | setConnection(connection: IConnection): void; 19 | 20 | /** 21 | * Set logger verbose mode 22 | * 23 | * @param {boolean} verbose 24 | * @returns {void} 25 | */ 26 | setVerbose(verbose: boolean): void; 27 | } 28 | 29 | export default ILoggerFactory; 30 | -------------------------------------------------------------------------------- /server/factory/INotifierFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import INotifier from "../service/notifier/INotifier"; 3 | import IFactory from "./IFactory"; 4 | 5 | /** 6 | * INotifier factory interface 7 | * 8 | * @module vscode-phpmd/server/factory 9 | * @author Sandhjé Bouw (sandhje@ecodes.io) 10 | */ 11 | interface INotifierFactory extends IFactory { 12 | /** 13 | * Set notifier connection 14 | * 15 | * @param {IConnection} connection 16 | * @returns {void} 17 | */ 18 | setConnection(connection: IConnection): void; 19 | } 20 | 21 | export default INotifierFactory; 22 | -------------------------------------------------------------------------------- /server/factory/NullLoggerFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import ILogger from "../service/logger/ILogger"; 3 | import NullLogger from "../service/logger/NullLogger"; 4 | import ILoggerFactory from "./ILoggerFactory"; 5 | 6 | /** 7 | * Null logger factory 8 | * 9 | * Creates a null object implementation of the ILogger interface 10 | * 11 | * @module vscode-phpmd/server/factory 12 | * @author Sandhjé Bouw (sandhje@ecodes.io) 13 | */ 14 | class NullLoggerFactory implements ILoggerFactory { 15 | /** 16 | * Create Null logger instance 17 | * 18 | * @see IFactory::create 19 | * @returns {ILogger} 20 | */ 21 | public create(): ILogger { 22 | return new NullLogger(); 23 | } 24 | 25 | /** 26 | * Null object implementation of set connection 27 | * 28 | * @see ILoggerFactory::setConnection 29 | * @param {IConnection} connection 30 | * @returns {void} 31 | */ 32 | public setConnection(connection: IConnection): void { 33 | return; 34 | } 35 | 36 | /** 37 | * Null object implementation of set verbose 38 | * 39 | * @see ILoggerFactory::setVerbose 40 | * @param {IConnection} connection 41 | * @returns {void} 42 | */ 43 | public setVerbose(verbose: boolean): void { 44 | return; 45 | } 46 | } 47 | 48 | export default NullLoggerFactory; 49 | -------------------------------------------------------------------------------- /server/factory/NullNotifierFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import INotifier from "../service/notifier/INotifier"; 3 | import NullNotifier from "../service/notifier/NullNotifier"; 4 | import INotifierFactory from "./INotifierFactory"; 5 | 6 | /** 7 | * Null notifier factory 8 | * 9 | * Creates a null object implementation of the INotifier interface 10 | * 11 | * @module vscode-phpmd/server/factory 12 | * @author Sandhjé Bouw (sandhje@ecodes.io) 13 | */ 14 | class NullNotifierFactory implements INotifierFactory { 15 | /** 16 | * Create Null notifier instance 17 | * 18 | * @see IFactory::create 19 | * @returns {INotifier} 20 | */ 21 | public create(): INotifier { 22 | return new NullNotifier(); 23 | } 24 | 25 | /** 26 | * Null object implementation of set connection 27 | * 28 | * @see INotifierFactory::setConnection 29 | * @param {IConnection} connection 30 | * @returns {void} 31 | */ 32 | public setConnection(connection: IConnection): void { 33 | return; 34 | } 35 | } 36 | 37 | export default NullNotifierFactory; 38 | -------------------------------------------------------------------------------- /server/factory/ParseTaskFactory.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import * as Xml2Js from "xml2js"; 3 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 4 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 5 | import ParseStrategy from "../service/pipeline/ParseStrategy"; 6 | import ILogger from "../service/logger/ILogger"; 7 | import NullLogger from "../service/logger/NullLogger"; 8 | import IFactory from "./IFactory"; 9 | 10 | /** 11 | * Parse task factory 12 | * 13 | * @module vscode-phpmd/server/factory 14 | * @author Sandhjé Bouw (sandhje@ecodes.io) 15 | */ 16 | class ExecuteProcessTaskFactory implements IFactory> { 17 | /** 18 | * @param {IPhpmdSettingsModels} settings 19 | * @param {ILogger} logger 20 | */ 21 | constructor( 22 | private settings: IPhpmdSettingsModel, 23 | private logger: ILogger = new NullLogger() 24 | ) { } 25 | 26 | /** 27 | * Create Parse task instance 28 | * 29 | * @see IFactory::create 30 | * @returns {Task} 31 | */ 32 | public create(): Task { 33 | let strategy = new ParseStrategy(this.getParser(), this.logger); 34 | 35 | return new Task(strategy); 36 | } 37 | 38 | /** 39 | * Get XML parser instance 40 | * 41 | * @returns {Xml2Js.Parser} 42 | */ 43 | protected getParser(): Xml2Js.Parser { 44 | return new Xml2Js.Parser(); 45 | } 46 | } 47 | 48 | export default ExecuteProcessTaskFactory; 49 | -------------------------------------------------------------------------------- /server/factory/PhpmdControllerFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import PhpmdController from "../controller/PhpmdController"; 3 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 4 | import IPhpmdEnvironmentModel from "../model/IPhpmdEnvironmentModel"; 5 | import IFactory from "./IFactory"; 6 | 7 | /** 8 | * PHP mess detector controller factory 9 | * 10 | * @module vscode-phpmd/server/factory 11 | * @author Sandhjé Bouw (sandhje@ecodes.io) 12 | */ 13 | class PhpmdControllerFactory implements IFactory { 14 | /** 15 | * @property {IConnection} connection 16 | */ 17 | private connection: IConnection; 18 | 19 | /** 20 | * @property {IPhpmdSettingsModel} settings 21 | */ 22 | private settings: IPhpmdSettingsModel; 23 | 24 | /** 25 | * @property {IPhpmdEnvironmentModel} environment 26 | */ 27 | private environment: IPhpmdEnvironmentModel; 28 | 29 | /** 30 | * Create PHP mess detector controller instance 31 | * 32 | * @see IFaction::create 33 | * @returns {PhpmdController} 34 | */ 35 | public create(): PhpmdController { 36 | if (!this.connection) { 37 | throw Error("Unable to create PhpmdController, no connection set"); 38 | } 39 | 40 | if (!this.settings) { 41 | throw Error("Unable to create PhpmdController, no settings set"); 42 | } 43 | 44 | return new PhpmdController(this.connection, this.settings, this.environment); 45 | } 46 | 47 | /** 48 | * Set VSCode client connection 49 | * 50 | * @param {IConnection} connection 51 | * @returns {void} 52 | */ 53 | public setConnection(connection: IConnection) { 54 | this.connection = connection; 55 | } 56 | 57 | /** 58 | * Set vscode-phpmd settings 59 | * 60 | * @param {IPhpmdSettingsModel} settings 61 | */ 62 | public setSettings(settings: IPhpmdSettingsModel) { 63 | this.settings = settings; 64 | } 65 | 66 | /** 67 | * Set vscode-phpmd environment 68 | * 69 | * @param {IPhpmdEnvironmentModel} environment 70 | */ 71 | public setEnvironment(environment: IPhpmdEnvironmentModel) { 72 | this.environment = environment; 73 | } 74 | } 75 | 76 | export default PhpmdControllerFactory; 77 | -------------------------------------------------------------------------------- /server/factory/PipelineFactory.ts: -------------------------------------------------------------------------------- 1 | import { Pipeline } from "@open-sourcerers/j-stillery"; 2 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 3 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 4 | import ILogger from "../service/logger/ILogger"; 5 | import NullLogger from "../service/logger/NullLogger"; 6 | import BuildDiagnosticsTaskFactory from "./BuildDiagnosticsTaskFactory"; 7 | import ExecuteProcessTaskFactory from "./ExecuteProcessTaskFactory"; 8 | import IFactory from "./IFactory"; 9 | import ParseTaskFactory from "./ParseTaskFactory"; 10 | import TestFileTaskFactory from "./TestFileTaskFactory"; 11 | import IPhpmdEnvironmentModel from "../model/IPhpmdEnvironmentModel"; 12 | 13 | /** 14 | * PHP mess detector validation pipeline factory 15 | * 16 | * @module vscode-phpmd/server/factory 17 | * @author Sandhjé Bouw (sandhje@ecodes.io) 18 | */ 19 | class PipelineFactory implements IFactory> { 20 | /** 21 | * @param {IPhpmdSettingsModel} settings 22 | * @param {ILogger} logger 23 | */ 24 | constructor( 25 | private settings: IPhpmdSettingsModel, 26 | private environment: IPhpmdEnvironmentModel, 27 | private logger: ILogger = new NullLogger() 28 | ) { } 29 | 30 | /** 31 | * Create validation pipeline instance 32 | * 33 | * Configure pipeline with tasks: 34 | * - ExecuteProcess task 35 | * - Parse task 36 | * - BuildDiagnostics task 37 | * 38 | * @see IFaction::create 39 | * @returns {Pipeline} 40 | */ 41 | public create(): Pipeline { 42 | let pipeline = new Pipeline() 43 | .pipe(new TestFileTaskFactory(this.settings, this.logger).create()) 44 | .pipe(new ExecuteProcessTaskFactory(this.settings, this.environment, this.logger).create()) 45 | .pipe(new ParseTaskFactory(this.settings, this.logger).create()) 46 | .pipe(new BuildDiagnosticsTaskFactory(this.settings, this.logger).create()); 47 | 48 | return pipeline; 49 | } 50 | } 51 | 52 | export default PipelineFactory; 53 | -------------------------------------------------------------------------------- /server/factory/PipelinePayloadFactory.ts: -------------------------------------------------------------------------------- 1 | import { Pipeline } from "@open-sourcerers/j-stillery"; 2 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 3 | import IFactory from "./IFactory"; 4 | 5 | /** 6 | * PHP mess detector pipeline payload model factory 7 | * 8 | * @module vscode-phpmd/server/factory 9 | * @author Sandhjé Bouw (sandhje@ecodes.io) 10 | */ 11 | class PipelinePayloadFactory implements IFactory { 12 | /** 13 | * @param {string} uri 14 | */ 15 | constructor( 16 | private uri: string 17 | ) { } 18 | 19 | /** 20 | * Set file URI to be used upon creating a new instance of the model 21 | * 22 | * @param uri 23 | */ 24 | public setUri(uri: string) { 25 | this.uri = uri; 26 | 27 | return this; 28 | } 29 | 30 | /** 31 | * Create pipeline payload model instance 32 | * 33 | * @see IFaction::create 34 | * @returns {PipelinePayloadModel} 35 | */ 36 | public create(): PipelinePayloadModel { 37 | return new PipelinePayloadModel(this.uri); 38 | } 39 | } 40 | 41 | export default PipelinePayloadFactory; 42 | -------------------------------------------------------------------------------- /server/factory/RemoteConsoleLoggerFactory.ts: -------------------------------------------------------------------------------- 1 | import { IConnection } from "vscode-languageserver"; 2 | import ILogger from "../service/logger/ILogger"; 3 | import RemoteConsoleLogger from "../service/logger/RemoteConsoleLogger"; 4 | import ILoggerFactory from "./ILoggerFactory"; 5 | 6 | /** 7 | * Remote console logger factory 8 | * 9 | * @module vscode-phpmd/server/factory 10 | * @author Sandhjé Bouw (sandhje@ecodes.io) 11 | */ 12 | class RemoteConsoleLoggerFactory implements ILoggerFactory { 13 | /** 14 | * @property {IConnection} connection 15 | */ 16 | private connection: IConnection; 17 | 18 | /** 19 | * @property {boolean} verbose 20 | */ 21 | private verbose: boolean = false; 22 | 23 | /** 24 | * Create remote console logger instance 25 | * 26 | * @see IFaction::create 27 | * @returns {ILogger} 28 | */ 29 | public create(): ILogger { 30 | if (!this.connection) { 31 | throw Error("Unable to create RemoteConsoleLogger, no connection set"); 32 | } 33 | 34 | return new RemoteConsoleLogger(this.connection.console, this.verbose); 35 | } 36 | 37 | /** 38 | * Set VSCode client connection 39 | * 40 | * @see ILoggerFactory::setConnection 41 | * @param {IConnection} connection 42 | * @returns {void} 43 | */ 44 | public setConnection(connection: IConnection) { 45 | this.connection = connection; 46 | } 47 | 48 | /** 49 | * Set verbose mode 50 | * 51 | * @see ILoggerFactory::setVerbose 52 | * @param {boolean} verbose 53 | * @returns {void} 54 | */ 55 | public setVerbose(verbose: boolean) { 56 | this.verbose = verbose; 57 | } 58 | } 59 | 60 | export default RemoteConsoleLoggerFactory; 61 | -------------------------------------------------------------------------------- /server/factory/TestFileTaskFactory.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import * as fs from "fs"; 3 | import IPhpmdSettingsModel from "../model/IPhpmdSettingsModel"; 4 | import PipelinePayloadModel from "../model/PipelinePayloadModel"; 5 | import ILogger from "../service/logger/ILogger"; 6 | import NullLogger from "../service/logger/NullLogger"; 7 | import TestFileStrategy from "../service/pipeline/TestFileStrategy"; 8 | import IFactory from "./IFactory"; 9 | 10 | /** 11 | * Test file task factory 12 | * 13 | * @module vscode-phpmd/server/factory 14 | * @author Sandhjé Bouw (sandhje@ecodes.io) 15 | */ 16 | class TestFileTaskFactory implements IFactory> { 17 | /** 18 | * Test file factory constructor 19 | * 20 | * @param {IPhpmdSettingsModel} settings 21 | * @param {ILogger} logger 22 | */ 23 | constructor( 24 | private settings: IPhpmdSettingsModel, 25 | private logger: ILogger = new NullLogger() 26 | ) { } 27 | 28 | /** 29 | * Create TestFile pipeline task instance 30 | * 31 | * @see IFactory::create 32 | * @returns {Task} 33 | */ 34 | public create(): Task { 35 | let strategy = new TestFileStrategy(fs.readFile, this.logger); 36 | 37 | return new Task(strategy); 38 | } 39 | } 40 | 41 | export default TestFileTaskFactory; 42 | -------------------------------------------------------------------------------- /server/init.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import Server from "./Server"; 4 | 5 | // Initialise the server and run 6 | (new Server()).main(); 7 | -------------------------------------------------------------------------------- /server/model/IPhpmdEnvironmentModel.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceFolder } from "vscode-languageserver"; 2 | 3 | /** 4 | * VSCode PHPMD environment model 5 | * 6 | * @module vscode-phpmd/server/model 7 | * @author Sandhjé Bouw (sandhje@ecodes.io) 8 | */ 9 | interface IPhpmdEnvironmentModel { 10 | /** 11 | * WorkspaceFolders 12 | * 13 | * @property {WorkspaceFolder[]} 14 | */ 15 | workspaceFolders: WorkspaceFolder[]; 16 | 17 | /** 18 | * OS Home dir 19 | * 20 | * @property {string} 21 | */ 22 | homeDir: string; 23 | } 24 | 25 | export default IPhpmdEnvironmentModel; 26 | -------------------------------------------------------------------------------- /server/model/IPhpmdSettingsModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * VSCode PHPMD settings model 3 | * 4 | * @module vscode-phpmd/server/model 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | interface IPhpmdSettingsModel { 8 | /** 9 | * Enabled 10 | * 11 | * @property {boolean} 12 | */ 13 | enabled: boolean; 14 | 15 | /** 16 | * PHP mess detector command 17 | * 18 | * @property {string} 19 | */ 20 | command: string; 21 | 22 | /** 23 | * PHP mess detector unsafe command enabled 24 | * 25 | * @property {boolean} 26 | */ 27 | unsafeCommandEnabled: boolean; 28 | 29 | /** 30 | * PHP mess detector unsafe command 31 | * 32 | * @property {string} 33 | */ 34 | unsafeCommand: string; 35 | 36 | /** 37 | * PHP mess detector rules 38 | * 39 | * @property {string} 40 | */ 41 | rules: string; 42 | 43 | /** 44 | * Verbose mode 45 | * 46 | * @property {boolean} 47 | */ 48 | verbose: boolean; 49 | 50 | /** 51 | * Clear errors on close 52 | * 53 | * @property {boolean} 54 | */ 55 | clearOnClose: boolean; 56 | } 57 | 58 | export default IPhpmdSettingsModel; 59 | -------------------------------------------------------------------------------- /server/model/PipelineErrorModel.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PHP mess detector validation pipeline error interface 3 | * 4 | * @module vscode-phpmd/server/model 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | class PipelineErrorModel { 8 | /** 9 | * Pipeline error model constructor 10 | * 11 | * @param {any} error Original error object 12 | * @param {boolean} silent Flag indicating wether the client should show a notification to the user or not 13 | */ 14 | public constructor( 15 | public error: any, 16 | public silent: boolean = false 17 | ) { } 18 | } 19 | 20 | export default PipelineErrorModel; 21 | -------------------------------------------------------------------------------- /server/model/PipelinePayloadModel.ts: -------------------------------------------------------------------------------- 1 | import { Diagnostic } from "vscode-languageserver"; 2 | import { URI } from "vscode-uri"; 3 | import { IPmd } from "./pmd"; 4 | 5 | /** 6 | * PHP mess detector validation pipeline payload interface 7 | * 8 | * @module vscode-phpmd/server/model 9 | * @author Sandhjé Bouw (sandhje@ecodes.io) 10 | */ 11 | class PipelinePayloadModel { 12 | /** 13 | * URI of file to be validated 14 | * 15 | * @property {string} 16 | */ 17 | public uri: string = ""; 18 | 19 | /** 20 | * File path for file to be validated 21 | * 22 | * @readonly 23 | * @property {string} 24 | */ 25 | public get path(): string { 26 | return URI.parse(this.uri).fsPath; 27 | } 28 | 29 | /** 30 | * Raw validation result 31 | * 32 | * @property {string} 33 | */ 34 | public raw: string = ""; 35 | 36 | /** 37 | * Parsed validation result 38 | * 39 | * @property {IPmd} 40 | */ 41 | public pmd: IPmd; 42 | 43 | /** 44 | * List of VSCode diagnosticts 45 | * 46 | * @property {Diagnostic[]} 47 | */ 48 | public diagnostics: Diagnostic[] = []; 49 | 50 | /** 51 | * Validation pipeline payload model constructor 52 | * 53 | * @param {string} uri URI of file to be validated 54 | */ 55 | public constructor(uri: string) { 56 | this.uri = uri; 57 | } 58 | } 59 | 60 | export default PipelinePayloadModel; 61 | -------------------------------------------------------------------------------- /server/model/pmd/IPmd.ts: -------------------------------------------------------------------------------- 1 | import { IPmdFileData } from "./IPmdFileData"; 2 | import { IPmdMetaData } from "./IPmdMetaData"; 3 | 4 | /** 5 | * PHP mess detector output interface 6 | * 7 | * @module vscode-phpmd/server/model/pmd 8 | * @author Sandhjé Bouw (sandhje@ecodes.io) 9 | */ 10 | export interface IPmd { 11 | /** 12 | * PHP mess detector metadata 13 | * 14 | * @property {IPmdMetaData} 15 | */ 16 | $: IPmdMetaData; 17 | 18 | /** 19 | * PHP mess detector file data 20 | * 21 | * @property {IPmdFileData} 22 | */ 23 | file: IPmdFileData[]; 24 | } 25 | -------------------------------------------------------------------------------- /server/model/pmd/IPmdFileData.ts: -------------------------------------------------------------------------------- 1 | import { IPmdFileMetaData } from "./IPmdFileMetaData"; 2 | import { IPmdViolation } from "./IPmdViolation"; 3 | 4 | /** 5 | * PHP mess detector file data interface 6 | * 7 | * @module vscode-phpmd/server/model/pmd 8 | * @author Sandhjé Bouw (sandhje@ecodes.io) 9 | */ 10 | export interface IPmdFileData { 11 | /** 12 | * PHP mess detector file metadata 13 | * 14 | * @property {IPmdFileMetaData} 15 | */ 16 | $: IPmdFileMetaData; 17 | 18 | /** 19 | * PHP mess detector violations list 20 | * 21 | * @property {IPmdViolation[]} 22 | */ 23 | violation: IPmdViolation[]; 24 | } 25 | -------------------------------------------------------------------------------- /server/model/pmd/IPmdFileMetaData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PHP mess detector file metadata interface 3 | * 4 | * @module vscode-phpmd/server/model/pmd 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | export interface IPmdFileMetaData { 8 | /** 9 | * Filename 10 | * 11 | * @property {string} 12 | */ 13 | name: string; 14 | } 15 | -------------------------------------------------------------------------------- /server/model/pmd/IPmdMetaData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PHP mess detector metadata interface 3 | * 4 | * @module vscode-phpmd/server/model/pmd 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | export interface IPmdMetaData { 8 | /** 9 | * Report timestamp 10 | * 11 | * @property {string} 12 | */ 13 | timestamp: string; 14 | 15 | /** 16 | * PHP mess detector version 17 | * 18 | * @property {string} 19 | */ 20 | version: string; 21 | } 22 | -------------------------------------------------------------------------------- /server/model/pmd/IPmdViolation.ts: -------------------------------------------------------------------------------- 1 | import { IPmdViolationMetaData } from "./IPmdViolationMetaData"; 2 | 3 | /** 4 | * PHP mess detector violation interface 5 | * 6 | * @module vscode-phpmd/server/model/pmd 7 | * @author Sandhjé Bouw (sandhje@ecodes.io) 8 | */ 9 | export interface IPmdViolation { 10 | /** 11 | * PHP mess detector violation message 12 | * 13 | * @property {string} 14 | */ 15 | _: string; 16 | 17 | /** 18 | * PHP mess detector violation metadata 19 | * 20 | * @property {IPmdViolationMetaData} 21 | */ 22 | $: IPmdViolationMetaData; 23 | } 24 | -------------------------------------------------------------------------------- /server/model/pmd/IPmdViolationMetaData.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PHP mess detector violation metadata interface 3 | * 4 | * @module vscode-phpmd/server/model/pmd 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | export interface IPmdViolationMetaData { 8 | /** 9 | * PHP mess detector violation begin line 10 | * 11 | * @property {string} 12 | */ 13 | beginline: string; 14 | 15 | /** 16 | * PHP mess detector violation class name 17 | * 18 | * @property {string} 19 | */ 20 | class: string; 21 | 22 | /** 23 | * PHP mess detector violation end line 24 | * 25 | * @property {string} 26 | */ 27 | endline: string; 28 | 29 | /** 30 | * PHP mess detector violation external info url 31 | * 32 | * @property {string} 33 | */ 34 | externalInfoUrl?: string; 35 | 36 | /** 37 | * PHP mess detector violation package 38 | * 39 | * @property {string} 40 | */ 41 | package: string; 42 | 43 | /** 44 | * PHP mess detector violation priority 45 | * 46 | * @property {string} 47 | */ 48 | priority: string; 49 | 50 | /** 51 | * PHP mess detector violation rule 52 | * 53 | * @property {string} 54 | */ 55 | rule: string; 56 | 57 | /** 58 | * PHP mess detector violation ruleset 59 | * 60 | * @property {string} 61 | */ 62 | ruleset: string; 63 | } 64 | -------------------------------------------------------------------------------- /server/model/pmd/index.ts: -------------------------------------------------------------------------------- 1 | export { IPmd } from "./IPmd"; 2 | export { IPmdFileData } from "./IPmdFileData"; 3 | export { IPmdFileMetaData } from "./IPmdFileMetaData"; 4 | export { IPmdMetaData } from "./IPmdMetaData"; 5 | export { IPmdViolation } from "./IPmdViolation"; 6 | export { IPmdViolationMetaData } from "./IPmdViolationMetaData"; 7 | -------------------------------------------------------------------------------- /server/service/PhpmdCommandBuilder.ts: -------------------------------------------------------------------------------- 1 | import ILogger from "./logger/ILogger"; 2 | import NullLogger from "./logger/NullLogger"; 3 | import { URI } from "vscode-uri"; 4 | import { WorkspaceFolder } from "vscode-languageserver"; 5 | 6 | /** 7 | * PHP mess detector command builder 8 | * 9 | * Builder for the php mess detector command 10 | * 11 | * @module vscode-phpmd/service 12 | * @author Sandhjé Bouw (sandhje@ecodes.io) 13 | */ 14 | class PhpmdCommandBuilder { 15 | /** 16 | * Logger 17 | * 18 | * @property {ILogger} 19 | */ 20 | private logger: ILogger; 21 | 22 | /** 23 | * Command 24 | * 25 | * @property {string} 26 | */ 27 | private readonly command: string; 28 | 29 | /** 30 | * Service constructor 31 | * 32 | * Takes the command string and variable for substitution 33 | * 34 | * @param {string} command 35 | * @param {WorkspaceFolder[]} workspaceFolders 36 | * @param {string} homeDir 37 | */ 38 | constructor(command: string, unsafeCommandEnabled: boolean, unsafeCommand: string, private workspaceFolders: WorkspaceFolder[], private homeDir: string) { 39 | let cmd = command; 40 | 41 | if (unsafeCommandEnabled && unsafeCommand) 42 | cmd = unsafeCommand; 43 | 44 | this.command = cmd; 45 | } 46 | 47 | public usingGlobalPhp(): boolean { 48 | return this.command.toLowerCase().substr(0, 4) === "php "; 49 | } 50 | 51 | /** 52 | * Build the PHP mess detector version command. 53 | * 54 | * @returns {string} 55 | */ 56 | public buildPhpmdVersionCommand(): string { 57 | const command = `${this.command} --version`; 58 | this.getLogger().info(`Building phpmd version command: ${command}`, true); 59 | 60 | return command; 61 | } 62 | 63 | /** 64 | * Build the PHP mess detector command. 65 | * 66 | * @param {string} options A string with PHP mess detector command line options 67 | * @returns {string} 68 | */ 69 | public buildPhpmdCommand(options: string): string { 70 | // Replace homedir in options 71 | if (options.indexOf("\"~/") > -1) 72 | options = options.replace(`"~/`, `"${this.homeDir}/`); 73 | 74 | if (options.indexOf("\"~\\") > -1) 75 | options = options.replace(`"~\\`, `"${this.homeDir}\\`); 76 | 77 | // Replace workspaceFolder in options 78 | if (options.indexOf("${workspaceFolder}") > -1) 79 | { 80 | const file = this.getFileFromOptions(options); 81 | const folder = this.getWorkspaceFolderForFile(file); 82 | 83 | if (file && folder) { 84 | options = options.replace("${workspaceFolder}", folder); 85 | } 86 | } 87 | 88 | const command = `${this.command} ${options}`; 89 | this.getLogger().info(`Building phpmd command: ${command}`, true); 90 | 91 | return command; 92 | } 93 | 94 | /** 95 | * Logger setter 96 | * 97 | * @param {ILogger} logger 98 | * @returns {void} 99 | */ 100 | public setLogger(logger: ILogger): void { 101 | this.logger = logger; 102 | } 103 | 104 | /** 105 | * Logger getter 106 | * 107 | * Returns null object logger if the setter was not called before 108 | * 109 | * @returns {ILogger} 110 | */ 111 | protected getLogger(): ILogger { 112 | if (!this.logger) { 113 | this.logger = new NullLogger(); 114 | } 115 | 116 | return this.logger; 117 | } 118 | 119 | protected getFileFromOptions(options: string): string { 120 | if (!options) 121 | return undefined; 122 | 123 | const optionsSegments = options.split(" "); 124 | const fileSegment = optionsSegments[0]; 125 | 126 | if (!fileSegment || fileSegment.length <= 2) 127 | return undefined; 128 | 129 | return fileSegment.substring(1, fileSegment.length - 1); 130 | } 131 | 132 | protected getWorkspaceFolderForFile(file: string): string { 133 | if (!file) 134 | return undefined; 135 | 136 | if (!this.workspaceFolders || !this.workspaceFolders.length) 137 | return undefined; 138 | 139 | let workspaceFolder = this.workspaceFolders.find(folder => { 140 | const folderPath = URI.parse(folder.uri).fsPath; 141 | const result = file.startsWith(folderPath); 142 | 143 | if (result) 144 | this.getLogger().info(`Found match between file and workspace folder (file: "${file}", workspaceFolder: "${folderPath}")`, true); 145 | 146 | return result; 147 | }); 148 | 149 | if (!workspaceFolder) { 150 | this.getLogger().info(`No matching workspace folder found for file "${file}"`, true); 151 | this.getLogger().info(`Using first folder in workspace folder array: "${URI.parse(this.workspaceFolders[0].uri).fsPath}"`, true); 152 | workspaceFolder = this.workspaceFolders[0]; 153 | } 154 | 155 | return URI.parse(workspaceFolder.uri).fsPath; 156 | } 157 | } 158 | 159 | export default PhpmdCommandBuilder; -------------------------------------------------------------------------------- /server/service/PhpmdService.ts: -------------------------------------------------------------------------------- 1 | import * as Process from "child_process"; 2 | import ILogger from "./logger/ILogger"; 3 | import NullLogger from "./logger/NullLogger"; 4 | import PhpmdCommandBuilder from "./PhpmdCommandBuilder"; 5 | 6 | /** 7 | * PHP mess detector service 8 | * 9 | * Interaction layer with the PHP mess detector command. 10 | * 11 | * @module vscode-phpmd/service 12 | * @author Sandhjé Bouw (sandhje@ecodes.io) 13 | */ 14 | class PhpmdService { 15 | /** 16 | * NodeJS process executor 17 | * 18 | * @property {(command: sstring) => Process.ChildProcess} 19 | */ 20 | private exec: (command: string) => Process.ChildProcess; 21 | 22 | /** 23 | * Logger 24 | * 25 | * @property {ILogger} 26 | */ 27 | private logger: ILogger; 28 | 29 | /** 30 | * Service constructor 31 | * 32 | * Takes the PHP mess detector command to be worked with 33 | * 34 | * @param {PhpmdCommandBuilder} commandBuilder 35 | */ 36 | constructor(private commandBuilder: PhpmdCommandBuilder) { } 37 | 38 | /** 39 | * PHP availability test 40 | * 41 | * Tests wether the PHP command is globally available on the system. Resolves with a boolean true 42 | * if available or if the PHP mess detector command does not use the PHP command. Rejects with an 43 | * error if neither of these conditions apply. 44 | * 45 | * @returns {Promise} 46 | */ 47 | public testPhp(): Promise { 48 | if (!this.commandBuilder.usingGlobalPhp()) { 49 | this.getLogger().info("PHP Mess Detector command not using global PHP, skipping PHP test", true); 50 | return Promise.resolve(true); 51 | } 52 | 53 | return new Promise((resolve, reject) => { 54 | this.execute("php -v").then((data) => { 55 | this.getLogger().info("PHP command test successful (" + data.substr(0, 16).trim() + " ...)", true); 56 | resolve(true); 57 | }, (err: Error) => { 58 | reject(err); 59 | }); 60 | }); 61 | } 62 | 63 | /** 64 | * PHP mess detector availability test 65 | * 66 | * Tests wether the PHP mess detector command can be executed. Resolves with a boolean true if 67 | * the command was successfully exectuted. Rejects with an error if not. 68 | * 69 | * @returns {Promise} 70 | */ 71 | public testPhpmd(): Promise { 72 | return new Promise((resolve, reject) => { 73 | this.testPhp().then(() => { 74 | return this.execute(this.commandBuilder.buildPhpmdVersionCommand()); 75 | }).then((data) => { 76 | this.getLogger().info("PHP Mess Detector test successful (" + data.trim() + ")", true); 77 | resolve(true); 78 | }, (err: Error) => { 79 | reject(err); 80 | }); 81 | }); 82 | } 83 | 84 | /** 85 | * Run the PHP mess detector command. 86 | * 87 | * Note: checking for availability of the command is the consumers responsibility and done through 88 | * the testPhpmd method. 89 | * 90 | * @param {string} options A string with PHP mess detector command line options 91 | * @returns {Promise} 92 | */ 93 | public run(options: string): Promise { 94 | this.getLogger().info(`Running phpmd command with options "${options}"`, true); 95 | return this.execute(this.commandBuilder.buildPhpmdCommand(options)); 96 | } 97 | 98 | /** 99 | * Logger setter 100 | * 101 | * @param {ILogger} logger 102 | * @returns {void} 103 | */ 104 | public setLogger(logger: ILogger): void { 105 | this.logger = logger; 106 | } 107 | 108 | /** 109 | * Executor setter 110 | * 111 | * Allows overriding the executor for testing purposes 112 | * 113 | * @param {(command: string) => Process.ChildProcess} exec 114 | * @returns {void} 115 | */ 116 | public setExecutor(exec: (command: string) => Process.ChildProcess) { 117 | this.exec = exec; 118 | } 119 | 120 | /** 121 | * Executor getter 122 | * 123 | * Returns NodeJS process executor if the setter was not called before. 124 | * 125 | * @returns {(command: string) => Process.ChildProcess} 126 | */ 127 | protected getExecutor(): (command: string) => Process.ChildProcess { 128 | if (!this.exec) { 129 | this.exec = Process.exec; 130 | } 131 | 132 | return this.exec; 133 | } 134 | 135 | /** 136 | * Logger getter 137 | * 138 | * Returns null object logger if the setter was not called before 139 | * 140 | * @returns {ILogger} 141 | */ 142 | protected getLogger(): ILogger { 143 | if (!this.logger) { 144 | this.logger = new NullLogger(); 145 | } 146 | 147 | return this.logger; 148 | } 149 | 150 | /** 151 | * Execute the passed command with this instance's executor 152 | * 153 | * @param {string} cmd 154 | * @returns {Promise} 155 | */ 156 | protected execute(cmd): Promise { 157 | return new Promise((resolve, reject) => { 158 | let result: string; 159 | let exec = this.getExecutor(); 160 | let process: Process.ChildProcess = exec(cmd); 161 | 162 | process.stdout.setEncoding("utf8"); 163 | 164 | process.stdout.on("data", (data) => { 165 | if (result) { 166 | data = result + data.toString(); 167 | } 168 | 169 | result = data.toString(); 170 | }); 171 | 172 | process.stdout.on("close", () => { 173 | if (!result) { 174 | reject(Error("An error occured, no output was received after executing the phpmd command")); 175 | return; 176 | } 177 | 178 | resolve(result); 179 | }); 180 | 181 | process.stdout.on("error", (err: Error) => { 182 | reject(err); 183 | }); 184 | }); 185 | } 186 | } 187 | 188 | export default PhpmdService; 189 | -------------------------------------------------------------------------------- /server/service/logger/ILogger.ts: -------------------------------------------------------------------------------- 1 | import { IConnection, ClientCapabilities, ServerCapabilities } from "vscode-languageserver"; 2 | 3 | /** 4 | * Logger interface 5 | * 6 | * @module vscode-phpmd/service/logger 7 | * @author Sandhjé Bouw (sandhje@ecodes.io) 8 | */ 9 | interface ILogger { 10 | /** 11 | * Set the verbose property flag 12 | * 13 | * @param {boolean} verbose The verbose flag 14 | * @returns {this} 15 | */ 16 | setVerbose(verbose: boolean): this; 17 | 18 | /** 19 | * Get the verbose property flag 20 | * 21 | * @returns {boolean} 22 | * @returns {this} 23 | */ 24 | getVerbose(): boolean; 25 | 26 | /** 27 | * Show an error message 28 | * 29 | * @param {string} message The message to show 30 | * @param {boolean} isVerbose If set to true only shows the message if the logger's verbose property is truthy 31 | * @returns {this} 32 | */ 33 | error(message: string, isVerbose?: boolean): this; 34 | 35 | /** 36 | * Show a warning message 37 | * 38 | * @param {string} message The message to show 39 | * @param {boolean} isVerbose If set to true only shows the message if the logger's verbose property is truthy 40 | * @returns {this} 41 | */ 42 | warn(message: string, isVerbose?: boolean): this; 43 | 44 | /** 45 | * Show an information message 46 | * 47 | * @param {string} message The message to show 48 | * @param {boolean} isVerbose If set to true only shows the message if the logger's verbose property is truthy 49 | * @returns {this} 50 | */ 51 | info(message: string, isVerbose?: boolean): this; 52 | 53 | /** 54 | * Log a message 55 | * 56 | * @param {string} message The message to log 57 | * @param {boolean} isVerbose If set to true only shows the message if the logger's verbose property is truthy 58 | * @returns {this} 59 | */ 60 | log(message: string, isVerbose?: boolean): this; 61 | 62 | /** 63 | * Attach the remote to the given connection. 64 | * 65 | * @param connection The connection this remote is operating on. 66 | */ 67 | attach(connection: IConnection): void; 68 | /** 69 | * The connection this remote is attached to. 70 | */ 71 | connection: IConnection; 72 | /** 73 | * Called to initialize the remote with the given 74 | * client capabilities 75 | * 76 | * @param capabilities The client capabilities 77 | */ 78 | initialize(capabilities: ClientCapabilities): void; 79 | /** 80 | * Called to fill in the server capabilities this feature implements. 81 | * 82 | * @param capabilities The server capabilities to fill. 83 | */ 84 | fillServerCapabilities(capabilities: ServerCapabilities): void; 85 | } 86 | 87 | export default ILogger; 88 | -------------------------------------------------------------------------------- /server/service/logger/NullLogger.ts: -------------------------------------------------------------------------------- 1 | import ILogger from "./ILogger"; 2 | import { IConnection, ClientCapabilities, ServerCapabilities } from "vscode-languageserver"; 3 | 4 | /** 5 | * Null object implementation of ILogger 6 | * 7 | * @module vscode-phpmd/service/logger 8 | * @author Sandhjé Bouw (sandhje@ecodes.io) 9 | */ 10 | class NullLogger implements ILogger { 11 | attach(connection: IConnection): void { 12 | throw new Error("Method not implemented."); 13 | } 14 | connection: IConnection; 15 | initialize(capabilities: ClientCapabilities): void { 16 | throw new Error("Method not implemented."); 17 | } 18 | fillServerCapabilities(capabilities: ServerCapabilities): void { 19 | throw new Error("Method not implemented."); 20 | } 21 | /** 22 | * Verbose flag 23 | * 24 | * @property {boolean} 25 | */ 26 | private verbose: boolean = false; 27 | 28 | /** 29 | * @see ILogger::setVerbose 30 | */ 31 | public setVerbose(verbose: boolean): this { 32 | this.verbose = verbose; 33 | 34 | return this; 35 | } 36 | 37 | /** 38 | * @see ILogger::getVerbose 39 | */ 40 | public getVerbose(): boolean { 41 | return this.verbose; 42 | } 43 | 44 | /** 45 | * @see ILogger::error 46 | */ 47 | public error(message: string, isVerbose?: boolean): this { 48 | return this; 49 | } 50 | 51 | /** 52 | * @see ILogger::warn 53 | */ 54 | public warn(message: string, isVerbose?: boolean): this { 55 | return this; 56 | } 57 | 58 | /** 59 | * @see ILogger::info 60 | */ 61 | public info(message: string, isVerbose?: boolean): this { 62 | return this; 63 | } 64 | 65 | /** 66 | * @see ILogger::log 67 | */ 68 | public log(message: string, isVerbose?: boolean): this { 69 | return this; 70 | } 71 | } 72 | 73 | export default NullLogger; 74 | -------------------------------------------------------------------------------- /server/service/logger/RemoteConsoleLogger.ts: -------------------------------------------------------------------------------- 1 | import { RemoteConsole, IConnection, ClientCapabilities, ServerCapabilities } from "vscode-languageserver"; 2 | import ILogger from "./ILogger"; 3 | 4 | /** 5 | * Remote console implementation of ILogger 6 | * 7 | * @module vscode-phpmd/service/logger 8 | * @author Sandhjé Bouw (sandhje@ecodes.io) 9 | */ 10 | class RemoteConsoleLogger implements ILogger { 11 | /** 12 | * Remote console logger constructor 13 | * 14 | * @param {RemoteConsole} remoteConsole 15 | * @param {boolean} verbose 16 | */ 17 | constructor(private remoteConsole: RemoteConsole, private verbose: boolean = false) {} 18 | 19 | /** 20 | * Attach the remote to the given connection. 21 | * 22 | * @param connection The connection this remote is operating on. 23 | */ 24 | attach(connection: IConnection): void { 25 | throw new Error("Method not implemented."); 26 | }; 27 | 28 | /** 29 | * The connection this remote is attached to. 30 | */ 31 | connection: IConnection; 32 | 33 | /** 34 | * Called to initialize the remote with the given 35 | * client capabilities 36 | * 37 | * @param capabilities The client capabilities 38 | */ 39 | initialize(capabilities: ClientCapabilities): void { 40 | throw new Error("Method not implemented."); 41 | } 42 | 43 | /** 44 | * Called to fill in the server capabilities this feature implements. 45 | * 46 | * @param capabilities The server capabilities to fill. 47 | */ 48 | fillServerCapabilities(capabilities: ServerCapabilities): void { 49 | throw new Error("Method not implemented."); 50 | } 51 | 52 | /** 53 | * @see ILogger::setVerbose 54 | */ 55 | public setVerbose(verbose: boolean): this { 56 | this.verbose = verbose; 57 | 58 | return this; 59 | } 60 | 61 | /** 62 | * @see ILogger::getVerbose 63 | */ 64 | public getVerbose(): boolean { 65 | return this.verbose; 66 | } 67 | 68 | /** 69 | * @see ILogger::error 70 | */ 71 | public error(message: string, isVerbose?: boolean): this { 72 | if (!isVerbose || this.getVerbose() === true) { 73 | this.remoteConsole.error(message); 74 | } 75 | 76 | return this; 77 | } 78 | 79 | /** 80 | * @see ILogger::warn 81 | */ 82 | public warn(message: string, isVerbose?: boolean): this { 83 | if (!isVerbose || this.getVerbose() === true) { 84 | this.remoteConsole.warn(message); 85 | } 86 | 87 | return this; 88 | } 89 | 90 | /** 91 | * @see ILogger::info 92 | */ 93 | public info(message: string, isVerbose?: boolean): this { 94 | if (!isVerbose || this.getVerbose() === true) { 95 | this.remoteConsole.info(message); 96 | } 97 | 98 | return this; 99 | } 100 | 101 | /** 102 | * @see ILogger::log 103 | */ 104 | public log(message: string, isVerbose?: boolean): this { 105 | if (!isVerbose || this.getVerbose() === true) { 106 | this.remoteConsole.log(message); 107 | } 108 | 109 | return this; 110 | } 111 | } 112 | 113 | export default RemoteConsoleLogger; 114 | -------------------------------------------------------------------------------- /server/service/notifier/ClientConnectionNotifier.ts: -------------------------------------------------------------------------------- 1 | import { IConnection, MessageType, ShowMessageNotification } from "vscode-languageserver"; 2 | import INotifier from "./INotifier"; 3 | 4 | /** 5 | * Client connection implementation of INotifier 6 | * 7 | * @module vscode-phpmd/service/notifier 8 | * @author Sandhjé Bouw (sandhje@ecodes.io) 9 | */ 10 | class ClientConnectionNotifier implements INotifier { 11 | /** 12 | * Client connection notifier constructor 13 | * 14 | * @param {IConnection} connection 15 | */ 16 | constructor(private connection: IConnection) {} 17 | 18 | /** 19 | * @see INotifier::error 20 | */ 21 | public error(message: string) { 22 | this.connection.sendNotification(ShowMessageNotification.type, { 23 | message, 24 | type: MessageType.Error, 25 | }); 26 | 27 | return this; 28 | } 29 | } 30 | 31 | export default ClientConnectionNotifier; 32 | -------------------------------------------------------------------------------- /server/service/notifier/INotifier.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Notifier interface 3 | * 4 | * @module vscode-phpmd/service/notifier 5 | * @author Sandhjé Bouw (sandhje@ecodes.io) 6 | */ 7 | interface INotifier { 8 | /** 9 | * Show an error notification to the user 10 | * 11 | * @param {string} message The message to show 12 | * @returns {this} 13 | */ 14 | error(message: string): this; 15 | } 16 | 17 | export default INotifier; 18 | -------------------------------------------------------------------------------- /server/service/notifier/NullNotifier.ts: -------------------------------------------------------------------------------- 1 | import INotifier from "./INotifier"; 2 | 3 | /** 4 | * Null object implementation of INotifier 5 | * 6 | * @module vscode-phpmd/service/notifier 7 | * @author Sandhjé Bouw (sandhje@ecodes.io) 8 | */ 9 | class NullNotifier implements INotifier { 10 | /** 11 | * @see INotifier::error 12 | */ 13 | public error(message: string): this { 14 | return this; 15 | } 16 | } 17 | 18 | export default NullNotifier; 19 | -------------------------------------------------------------------------------- /server/service/pipeline/BuildDiagnosticsStrategy.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteStrategy } from "@open-sourcerers/j-stillery"; 2 | import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; 3 | import PipelinePayloadModel from "../../model/PipelinePayloadModel"; 4 | import { IPmd, IPmdViolation } from "../../model/pmd"; 5 | import ILogger from "../logger/ILogger"; 6 | import NullLogger from "../logger/NullLogger"; 7 | 8 | /** 9 | * Build diagnostic pipeline task strategy 10 | * 11 | * Strategy used to create the pipeline task responsible for building 12 | * the diagnostic object from the parsed PHP mess detector result. 13 | * 14 | * @module vscode-phpmd/service/pipeline 15 | * @author Sandhjé Bouw (sandhje@ecodes.io) 16 | */ 17 | class BuildDiagnosticsStrategy implements IExecuteStrategy { 18 | /** 19 | * List of reported PHP mess detector violations 20 | * 21 | * Used to prevent reporting the same violation multiple times. 22 | * 23 | * @property {string[]} reported 24 | */ 25 | protected reported: string[] = []; 26 | 27 | /** 28 | * Map of diagnostic severity levels to PHP mess detector severity levels 29 | * 30 | * List indexes are used as the identifiers of the PHP mess detector sevetity level 31 | * 32 | * @property {DiagnosticSeverity[]} severityMap 33 | */ 34 | protected severityMap: DiagnosticSeverity[] = [ 35 | DiagnosticSeverity.Error, 36 | DiagnosticSeverity.Warning, 37 | DiagnosticSeverity.Information, 38 | DiagnosticSeverity.Hint, 39 | DiagnosticSeverity.Hint 40 | ]; 41 | 42 | /** 43 | * Build diagnostics task strategy constructor 44 | * 45 | * @param {ILogger} logger Logger, defaults to Null object implementation 46 | */ 47 | public constructor( 48 | private logger: ILogger = new NullLogger() 49 | ) { } 50 | 51 | /** 52 | * Strategy executor 53 | * 54 | * Resolve with a list of diagnostics from parsed PHP mess detector violations. 55 | * 56 | * @see IExecuteStrategy::execute 57 | */ 58 | public execute( 59 | input: PipelinePayloadModel, 60 | resolve: (output?: PipelinePayloadModel | PromiseLike) => void, 61 | reject: (reason: any) => void 62 | ) { 63 | let pmd = input.pmd; 64 | 65 | if (pmd === null || typeof pmd.file === "undefined") { 66 | // If no pmd results found, resolve without diagnostics 67 | resolve(input); 68 | return; 69 | } 70 | 71 | // For all files in Pmd result 72 | // Get diagnostics and append to payload 73 | pmd.file.every((file) => { 74 | input.diagnostics = input.diagnostics.concat(this.getDiagnostics(file.$.name, this.getProblems(pmd))); 75 | 76 | return true; 77 | }); 78 | 79 | resolve(input); 80 | }; 81 | 82 | /** 83 | * Get a list of violations from the parsed PHP mess detector result 84 | * 85 | * @param {IPmd} pmd 86 | * @returns {IPmdViolation[]} 87 | */ 88 | protected getProblems(pmd: IPmd): IPmdViolation[] { 89 | return pmd.file[0].violation || []; 90 | } 91 | 92 | /** 93 | * Create a list of diagnostics from a list of PHP mess detector violations 94 | * 95 | * @param {string} filename 96 | * @param {IPmdViolation[]} problems 97 | * @returns {Diagnostic[]} 98 | */ 99 | protected getDiagnostics(filename: string, problems: IPmdViolation[]): Diagnostic[] { 100 | let diagnostics: Diagnostic[] = []; 101 | 102 | this.clearReported(); // Clear the already reported diagnostics to prevent false positives upon de-duplication 103 | 104 | problems.forEach((problem) => { 105 | try { 106 | let diagnostic = this.getDiagnostic(problem); 107 | 108 | if (diagnostic !== null) { 109 | diagnostics.push(diagnostic); 110 | } 111 | } catch (e) { 112 | this.logger.error("Error while parsing diagnostic info from PHP mess detector violation."); 113 | } 114 | }); 115 | 116 | return diagnostics; 117 | } 118 | 119 | /** 120 | * Create a diagnostic object from a PHP mess detector violation 121 | * 122 | * @throws {Error} If vscode diagnostic object could not be created from PHP mess detector violation 123 | * 124 | * @param {IPmdViolation} problem 125 | * @returns {Diagnostic} 126 | */ 127 | protected getDiagnostic(problem: IPmdViolation): Diagnostic { 128 | try { 129 | let line = this.getLine(problem); 130 | let index = this.getIndex(problem); 131 | let length = this.getLength(problem); 132 | 133 | let hash = this.createProblemHash(problem); 134 | if (hash !== null && this.alreadyReported(hash)) { 135 | return null; 136 | } 137 | 138 | this.report(hash); 139 | return { 140 | message: this.getMessage(problem), 141 | range: { 142 | end: { 143 | character: index + length, 144 | line 145 | }, 146 | start: { 147 | character: index, 148 | line 149 | }, 150 | }, 151 | severity: this.getSeverity(problem), 152 | source: this.getSource(problem) 153 | }; 154 | } catch (e) { 155 | throw new Error("Unable to create diagnostic (" + e.message + ")"); 156 | } 157 | } 158 | 159 | /** 160 | * Add a PHP mess detector violation hash to the reported list to prevent duplicate diagnostics 161 | * 162 | * @param {string} hash 163 | * @returns {void} 164 | */ 165 | protected report(hash: string): void { 166 | this.reported.push(hash); 167 | } 168 | 169 | /** 170 | * Check wether the passed violation hash was already reported 171 | * 172 | * @param {string} hash 173 | * @returns {boolean} 174 | */ 175 | protected alreadyReported(hash: string): boolean { 176 | return this.reported.indexOf(hash) >= 0; 177 | } 178 | 179 | /** 180 | * Clear the already reported problems 181 | * 182 | * @returns {void} 183 | */ 184 | protected clearReported(): void { 185 | this.reported = []; 186 | } 187 | 188 | /** 189 | * Create a problem hash from a PHP mess detector violation 190 | * 191 | * @param {IPmdViolation} problem 192 | * @returns {string} 193 | */ 194 | protected createProblemHash(problem: IPmdViolation): string { 195 | let ruleset = problem.$.ruleset || null; 196 | let rule = problem.$.rule || null; 197 | let line = problem.$.beginline || null; 198 | 199 | let str = ruleset + "-" + rule + "-" + line; 200 | 201 | let hash = 0; 202 | if (str.length === 0) { 203 | return null; 204 | } 205 | 206 | for (let i = 0; i < str.length; i++) { 207 | let char = str.charCodeAt(i); 208 | hash = ((hash << 5) - hash) + char; 209 | hash = hash & hash; // Convert to 32bit integer 210 | } 211 | 212 | return hash.toString(); 213 | } 214 | 215 | /** 216 | * Get the violation line number from a PHP mess detector violation 217 | * 218 | * @throws {Error} If line number could not be extracted from violation 219 | * 220 | * @param {IPmdViolation} problem 221 | * @return {number} 222 | */ 223 | protected getLine(problem: IPmdViolation): number { 224 | let beginline = problem.$.beginline || null; 225 | 226 | if (beginline === null) { 227 | throw new Error("Unable to find problem begin line"); 228 | } 229 | 230 | return parseInt(beginline, 10) - 1; 231 | } 232 | 233 | /** 234 | * Get the violation start index from a PHP mess detector violation 235 | * 236 | * Returns a fixed value of 0 since PHP mess detector violations do not contain a start index. 237 | * 238 | * @param {IPmdViolation} problem 239 | * @return {number} 240 | */ 241 | protected getIndex(problem: IPmdViolation): number { 242 | return 0; 243 | } 244 | 245 | /** 246 | * Get the violation length from a PHP mess detector violation 247 | * 248 | * Returns a fixed value of Number.MAX_VALUE since PHP mess detector violations do not contain a length. 249 | * 250 | * @param {IPmdViolation} problem 251 | * @return {number} 252 | */ 253 | protected getLength(problem: IPmdViolation): number { 254 | return Number.MAX_VALUE; 255 | } 256 | 257 | /** 258 | * Get the violation message from a PHP mess detector violation 259 | * 260 | * @throws {Error} If message could not be extracted from violation 261 | * 262 | * @param {IPmdViolation} problem 263 | * @return {string} 264 | */ 265 | protected getMessage(problem: IPmdViolation): string { 266 | let message = problem._ || null; 267 | 268 | message = message.replace(/^\s+|\s+$/g, ''); 269 | 270 | if (message === null) { 271 | throw new Error("Unable to find problem message"); 272 | } 273 | 274 | return message; 275 | } 276 | 277 | /** 278 | * Get the source from a PHP mess detector violation 279 | * 280 | * Returns a fixed value of "PHP Mess Detector" since PHP mess detector violations do not contain a source. 281 | * 282 | * @param {IPmdViolation} problem 283 | * @return {number} 284 | */ 285 | protected getSource(problem: IPmdViolation): string { 286 | return "PHP Mess Detector"; 287 | } 288 | 289 | /** 290 | * Get the violation severity from a PHP mess detector violation 291 | * 292 | * Maps PHP mess detector severities to vscode diagnostic severities using the severityMap property. If no 293 | * mapping was possible defaults to a severity of "Hint". 294 | * 295 | * @param {IPmdViolation} problem 296 | * @return {DiagnosticSeverity} 297 | */ 298 | protected getSeverity(problem: any): DiagnosticSeverity { 299 | let priority: number = parseInt(problem.$.priority, 10) || 100; 300 | 301 | let severity = this.severityMap[priority - 1] || DiagnosticSeverity.Hint; 302 | 303 | return severity; 304 | } 305 | } 306 | 307 | export default BuildDiagnosticsStrategy; 308 | -------------------------------------------------------------------------------- /server/service/pipeline/ExecuteProcessStrategy.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteStrategy } from "@open-sourcerers/j-stillery"; 2 | import * as Process from "child_process"; 3 | import PipelineErrorModel from "../../model/PipelineErrorModel"; 4 | import PipelinePayloadModel from "../../model/PipelinePayloadModel"; 5 | import ILogger from "../logger/ILogger"; 6 | import NullLogger from "../logger/NullLogger"; 7 | import PhpmdService from "../PhpmdService"; 8 | import PhpmdCommandBuilder from "../PhpmdCommandBuilder"; 9 | 10 | /** 11 | * Execute PHP mess detector pipeline task strategy 12 | * 13 | * Strategy used to create the pipeline task responsible for executing 14 | * the PHP mess detector command. 15 | * 16 | * @module vscode-phpmd/service/pipeline 17 | * @author Sandhjé Bouw (sandhje@ecodes.io) 18 | */ 19 | class ExecuteProcessStrategy implements IExecuteStrategy { 20 | /** 21 | * PHP mess detector service 22 | * 23 | * @property {PhpmdService} service 24 | */ 25 | private service: PhpmdService; 26 | 27 | /** 28 | * Execute process task strategy constructor 29 | * 30 | * @param {PhpmdCommandBuilder} commandBuilder CommandBuilder used to instantiate the PHP mess detector service 31 | * @param {string} rules PHP mess detector rules to be passed as option to the command 32 | * @param {ILogger} logger Logger, defaults to Null object implementation 33 | */ 34 | public constructor( 35 | private commandBuilder: PhpmdCommandBuilder, 36 | private rules: string, 37 | private logger: ILogger = new NullLogger() 38 | ) { } 39 | 40 | /** 41 | * Strategy executor 42 | * 43 | * Resolve with the result from the executed PHP mess detector command. 44 | * 45 | * @see IExecuteStrategy::execute 46 | */ 47 | public execute( 48 | input: PipelinePayloadModel, 49 | resolve: (output?: PipelinePayloadModel | PromiseLike) => void, 50 | reject: (reason: any) => void 51 | ) { 52 | this.executeProcess(input.path).then((data) => { 53 | input.raw = this.ltrimXML(data); 54 | 55 | resolve(input); 56 | }, (err: Error) => { 57 | reject(new PipelineErrorModel(err, false)); 58 | }); 59 | }; 60 | 61 | /** 62 | * PHP mess detector service setter 63 | * 64 | * Allows overriding of the PHP mess detector service for testing purposes. 65 | * 66 | * @param {PhpmdService} service 67 | */ 68 | public setService(service: PhpmdService) { 69 | this.service = service; 70 | } 71 | 72 | /** 73 | * PHP mess detector service getter 74 | * 75 | * Creates an instance of the default PHP mess detector service if no other instance was set before. 76 | * 77 | * @returns {PhpmdService} 78 | */ 79 | protected getService() { 80 | if (!this.service) { 81 | this.service = new PhpmdService(this.commandBuilder); 82 | this.service.setLogger(this.logger); 83 | } 84 | 85 | return this.service; 86 | } 87 | 88 | /** 89 | * Strip any leading text or whitespace before the xml opening tag 90 | * 91 | * @param {string} xml 92 | * @returns {string} 93 | */ 94 | protected ltrimXML(xml: string): string { 95 | return xml.substring(xml.indexOf("} 103 | */ 104 | protected executeProcess(path: string): Promise { 105 | return this.getService().run(`"${path}" xml "${this.rules}"`); 106 | } 107 | } 108 | 109 | export default ExecuteProcessStrategy; 110 | -------------------------------------------------------------------------------- /server/service/pipeline/ParseStrategy.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteStrategy } from "@open-sourcerers/j-stillery"; 2 | import { Parser } from "xml2js"; 3 | import PipelinePayloadModel from "../../model/PipelinePayloadModel"; 4 | import ILogger from "../logger/ILogger"; 5 | import { IPmd } from "../../model/pmd"; 6 | 7 | /** 8 | * Parse pipeline task strategy 9 | * 10 | * Strategy used to create the pipeline task responsible for parsing 11 | * the PHP mess detector result. 12 | * 13 | * @module vscode-phpmd/service/pipeline 14 | * @author Sandhjé Bouw (sandhje@ecodes.io) 15 | */ 16 | class ParseStrategy implements IExecuteStrategy { 17 | /** 18 | * Parse task strategy constructor 19 | * 20 | * @param {Parser} parser 21 | * @param {ILogger} logger 22 | */ 23 | public constructor( 24 | private parser: Parser, 25 | private logger: ILogger 26 | ) { } 27 | 28 | /** 29 | * Strategy executor 30 | * 31 | * Resolve with the parse PHP mess detector result. 32 | * 33 | * @see IExecuteStrategy::execute 34 | */ 35 | public execute( 36 | input: PipelinePayloadModel, 37 | resolve: (output?: PipelinePayloadModel | PromiseLike) => void, 38 | reject: (reason: any) => void 39 | ) { 40 | this.parser.parseString(this.escapeInput(input), (error, result) => { 41 | if (error) { 42 | this.logger.log(`Unable to parse result xml. ${error}`); 43 | } 44 | 45 | if (!result) { 46 | result = { pmd: null }; 47 | } 48 | 49 | input.pmd = result.pmd; 50 | 51 | resolve(input); 52 | }); 53 | }; 54 | 55 | private escapeInput(input: PipelinePayloadModel): string { 56 | const escapedPath = input.path.replace(/[<>&'"]/g, function (c) { 57 | switch (c) { 58 | case '<': return '<'; 59 | case '>': return '>'; 60 | case '&': return '&'; 61 | case '\'': return '''; 62 | case '"': return '"'; 63 | } 64 | }); 65 | 66 | if (escapedPath !== input.path) { 67 | this.logger.info(`Escaping path in phpmd output xml to prevent parsing errors. Replacing ${input.path} with ${escapedPath}`, true); 68 | 69 | const trimmedPath = /^[a-zA-Z]:/.test(input.path) ? input.path.substr(2) : input.path; 70 | const trimmedEscapedPath = /^[a-zA-Z]:/.test(escapedPath) ? escapedPath.substr(2) : escapedPath; 71 | 72 | return input.raw.replace(trimmedPath, trimmedEscapedPath); 73 | } 74 | 75 | return input.raw; 76 | } 77 | } 78 | 79 | export default ParseStrategy; 80 | -------------------------------------------------------------------------------- /server/service/pipeline/TestFileStrategy.ts: -------------------------------------------------------------------------------- 1 | import { IExecuteStrategy } from "@open-sourcerers/j-stillery"; 2 | import * as fs from "fs"; 3 | import PipelineErrorModel from "../../model/PipelineErrorModel"; 4 | import PipelinePayloadModel from "../../model/PipelinePayloadModel"; 5 | import ILogger from "../logger/ILogger"; 6 | import NullLogger from "../logger/NullLogger"; 7 | 8 | /** 9 | * Test file pipeline task strategy 10 | * 11 | * Strategy used to test wether the file to be analyzed by PHPMD is readable 12 | * 13 | * @module vscode-phpmd/service/pipeline 14 | * @author Sandhjé Bouw (sandhje@ecodes.io) 15 | */ 16 | class TestFileStrategy implements IExecuteStrategy { 17 | /** 18 | * Test file task strategy constructor 19 | * 20 | * @param {IReadFile} readfile 21 | * @param {ILogger} logger 22 | */ 23 | public constructor( 24 | private readfile: IReadFile, 25 | private logger: ILogger = new NullLogger() 26 | ) { } 27 | 28 | /** 29 | * Strategy executor 30 | * 31 | * Test wether the file is in the input is readable 32 | * 33 | * @see IExecuteStrategy::execute 34 | */ 35 | public execute( 36 | input: PipelinePayloadModel, 37 | resolve: (output?: PipelinePayloadModel | PromiseLike) => void, 38 | reject: (reason: any) => void 39 | ) { 40 | this.readfile(input.path, "utf8", (err: NodeJS.ErrnoException, data: string) => { 41 | // Reject the pipeline if the file was not found or is not readable 42 | if (err) { 43 | this.logger.error("File " + input.path + " not found"); 44 | return reject(new PipelineErrorModel(err, true)); 45 | } 46 | 47 | // Resolve with unmodified input if the file is readable 48 | this.logger.info("File " + input.path + " test successful", true); 49 | return resolve(input); 50 | }); 51 | }; 52 | } 53 | 54 | /** 55 | * Readfile interface 56 | * 57 | * @module vscode-phpmd/service/pipeline 58 | * @author Sandhjé Bouw (sandhje@ecodes.io) 59 | */ 60 | interface IReadFile { 61 | (filename: string, encoding: string, callback: (err: NodeJS.ErrnoException, data: string) => void): void; 62 | } 63 | 64 | export default TestFileStrategy; 65 | -------------------------------------------------------------------------------- /tests/controller/PhpmdControllerTest.ts: -------------------------------------------------------------------------------- 1 | import { Pipeline } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import { Diagnostic, IConnection, Position, Range, TextDocumentIdentifier, WorkspaceFolder } from "vscode-languageserver"; 6 | import PhpmdController from "../../server/controller/PhpmdController"; 7 | import PipelinePayloadFactory from "../../server/factory/PipelinePayloadFactory"; 8 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 9 | import PipelineErrorModel from "../../server/model/PipelineErrorModel"; 10 | import PipelinePayloadModel from "../../server/model/PipelinePayloadModel"; 11 | import NullLogger from "../../server/service/logger/NullLogger"; 12 | import NullNotifier from "../../server/service/notifier/NullNotifier"; 13 | import PhpmdService from "../../server/service/PhpmdService"; 14 | import IPhpmdEnvironmentModel from "../../server/model/IPhpmdEnvironmentModel"; 15 | 16 | @suite("PhpMD controller") 17 | class PhpmdControllerTest { 18 | 19 | @test("Should send diagnostics") 20 | public assertSendDiagnostics(done) { 21 | // Arrange 22 | // ======= 23 | // Fake settings 24 | let settings = { 25 | enabled: true, 26 | command: "test", 27 | rules: "cleancode,codesize,controversial,design,unusedcode,naming", 28 | verbose: true 29 | }; 30 | 31 | // Fake environment 32 | let environment = { 33 | homeDir: "/home/JohnDoe", 34 | workspaceFolders: [ 35 | { 36 | name: "www", 37 | uri: "file://var/www" 38 | } 39 | ] 40 | }; 41 | 42 | // Fake document 43 | let document = { 44 | uri: "test" 45 | }; 46 | 47 | // Fake diagnostic 48 | let diagnostic = {}; 49 | diagnostic.range = { 50 | end: { 51 | character: Number.MAX_VALUE, 52 | line: 0 53 | }, 54 | start: { 55 | character: 0, 56 | line: 0 57 | } 58 | }; 59 | diagnostic.severity = 1; 60 | diagnostic.code = null; 61 | diagnostic.source = "Test"; 62 | diagnostic.message = "Lorem ipsum dolor"; 63 | 64 | // GetVersion stub 65 | let testPhpmdStub = sinon.stub(); 66 | testPhpmdStub.returns(Promise.resolve(true)); 67 | 68 | // Fake service 69 | let service = {}; 70 | service.testPhpmd = testPhpmdStub; 71 | 72 | // Fake pipeline payload 73 | let payload = {}; 74 | payload.uri = document.uri; 75 | payload.diagnostics = [diagnostic]; 76 | 77 | // Stub connection 78 | let connection = {}; 79 | connection.sendDiagnostics = (params) => { 80 | // Assert 81 | // ====== 82 | // Expect params to match fakes 83 | expect(params.uri).to.equal(document.uri); 84 | expect(params.diagnostics).to.equal(payload.diagnostics); 85 | }; 86 | 87 | // Stub pipeline payload factory 88 | let pipelinePayloadFactory = {}; 89 | pipelinePayloadFactory.setUri = (uri: string) => { return pipelinePayloadFactory; }; 90 | pipelinePayloadFactory.create = () => { return payload; }; 91 | 92 | // Stub pipeline 93 | let pipeline = new Pipeline(); 94 | 95 | // Create and configure controller 96 | let controller = new PhpmdController(connection, settings, environment); 97 | controller.setPipeline(pipeline); 98 | controller.setPipelinePayloadFactory(pipelinePayloadFactory); 99 | controller.setLogger(new NullLogger()); 100 | controller.setService(service); 101 | 102 | // Act 103 | // === 104 | controller.validate(document).then((result) => { 105 | // Assert 106 | // ====== 107 | // Expect result to equal true 108 | expect(result).to.equal(true); 109 | done(); 110 | }); 111 | } 112 | 113 | @test("Should test PHPMD command") 114 | public assertTestPhpmd(done) { 115 | // Arrange 116 | // ======= 117 | // Fake settings 118 | let settings = { 119 | enabled: true, 120 | command: "test", 121 | rules: "cleancode,codesize,controversial,design,unusedcode,naming", 122 | verbose: true 123 | }; 124 | 125 | // Fake environment 126 | let environment = { 127 | homeDir: "/home/JohnDoe", 128 | workspaceFolders: [ 129 | { 130 | name: "www", 131 | uri: "file://var/www" 132 | } 133 | ] 134 | }; 135 | 136 | // Fake document 137 | let document = { 138 | uri: "test" 139 | }; 140 | 141 | // GetVersion stub 142 | let testPhpmdStub = sinon.stub(); 143 | testPhpmdStub.returns(Promise.reject(Error("Test error"))); 144 | 145 | // Fake service 146 | let service = {}; 147 | service.testPhpmd = testPhpmdStub; 148 | 149 | // Stub connection 150 | let connection = {}; 151 | 152 | // Create and configure controller 153 | let controller = new PhpmdController(connection, settings, environment); 154 | controller.setLogger(new NullLogger()); 155 | controller.setNotifier(new NullNotifier()); 156 | controller.setService(service); 157 | 158 | // Act 159 | // === 160 | controller.validate(document).then(null, (err: Error) => { 161 | // Assert 162 | // ====== 163 | expect(err.message).to.equal("Test error"); 164 | done(); 165 | }); 166 | } 167 | 168 | @test("Should reject on error in pipeline") 169 | public assertRejectOnError(done) { 170 | // Arrange 171 | // ======= 172 | // Fake settings 173 | let settings = { 174 | enabled: true, 175 | command: "test", 176 | rules: "cleancode,codesize,controversial,design,unusedcode,naming", 177 | verbose: true 178 | }; 179 | 180 | // Fake environment 181 | let environment = { 182 | homeDir: "/home/JohnDoe", 183 | workspaceFolders: [ 184 | { 185 | name: "www", 186 | uri: "file://var/www" 187 | } 188 | ] 189 | }; 190 | 191 | // Fake document 192 | let document = { 193 | uri: "test" 194 | }; 195 | 196 | // GetVersion stub 197 | let testPhpmdStub = sinon.stub(); 198 | testPhpmdStub.returns(Promise.resolve(true)); 199 | 200 | // Fake service 201 | let service = {}; 202 | service.testPhpmd = testPhpmdStub; 203 | 204 | // Fake pipeline payload 205 | let payload = {}; 206 | payload.uri = document.uri; 207 | 208 | // Stub connection 209 | let connection = {}; 210 | 211 | // Stub pipeline payload factory 212 | let pipelinePayloadFactory = {}; 213 | pipelinePayloadFactory.setUri = (uri: string) => { return pipelinePayloadFactory; }; 214 | pipelinePayloadFactory.create = () => { return payload; }; 215 | 216 | // Stub pipeline run 217 | let runStub = sinon.stub(); 218 | runStub.rejects(new PipelineErrorModel("Test error")); 219 | 220 | // Stub pipeline 221 | let pipeline = new Pipeline(); 222 | pipeline.run = runStub; 223 | 224 | // Create and configure controller 225 | let controller = new PhpmdController(connection, settings, environment); 226 | controller.setPipeline(pipeline); 227 | controller.setPipelinePayloadFactory(pipelinePayloadFactory); 228 | controller.setService(service); 229 | 230 | // Act 231 | // === 232 | controller.validate(document).then(null, (err: PipelineErrorModel) => { 233 | // Assert 234 | // ====== 235 | expect(err).to.equal("Test error"); 236 | done(); 237 | }); 238 | } 239 | 240 | @test("Should not check if disabled") 241 | public assertExtensionDisabled(done) { 242 | // Arrange 243 | // ======= 244 | // Fake settings 245 | let settings = { 246 | enabled: false, 247 | command: "test", 248 | rules: "cleancode,codesize,controversial,design,unusedcode,naming", 249 | verbose: true 250 | }; 251 | 252 | // Fake environment 253 | let environment = { 254 | homeDir: "/home/JohnDoe", 255 | workspaceFolders: [ 256 | { 257 | name: "www", 258 | uri: "file://var/www" 259 | } 260 | ] 261 | }; 262 | 263 | // Fake document 264 | let document = { 265 | uri: "test" 266 | }; 267 | 268 | // Stub connection 269 | let connection = {}; 270 | connection.sendDiagnostics = sinon.spy(); 271 | 272 | // Create and configure controller 273 | let controller = new PhpmdController(connection, settings, environment); 274 | 275 | // Act 276 | // === 277 | controller.validate(document).then(() => { 278 | // Assert 279 | // ====== 280 | expect((connection.sendDiagnostics).notCalled).to.be.true; 281 | done(); 282 | }); 283 | } 284 | 285 | @test("Should clear diagnostics") 286 | public assertClear(done) { 287 | // Arrange 288 | // ======= 289 | // Fake settings 290 | let settings = { 291 | enabled: true, 292 | command: "test", 293 | rules: "cleancode,codesize,controversial,design,unusedcode,naming", 294 | verbose: true, 295 | clearOnClose: true 296 | }; 297 | 298 | // Fake environment 299 | let environment = { 300 | homeDir: "/home/JohnDoe", 301 | workspaceFolders: [ 302 | { 303 | name: "www", 304 | uri: "file://var/www" 305 | } 306 | ] 307 | }; 308 | 309 | // Fake document 310 | let document = { 311 | uri: "test" 312 | }; 313 | 314 | // Stub connection 315 | let connection = {}; 316 | connection.sendDiagnostics = (diagnosticParams => { 317 | // Assert 318 | // ====== 319 | expect(diagnosticParams.diagnostics).to.be.empty; 320 | done(); 321 | }); 322 | 323 | // Create and configure controller 324 | let controller = new PhpmdController(connection, settings, environment); 325 | controller.setLogger(new NullLogger()); 326 | 327 | // Act 328 | // === 329 | controller.clear(document); 330 | } 331 | 332 | 333 | @test("Should not clear diagnostics if disabled in settings") 334 | public assertClearDisabled() { 335 | // Arrange 336 | // ======= 337 | // Fake settings 338 | let settings = { 339 | enabled: true, 340 | command: "test", 341 | rules: "cleancode,codesize,controversial,design,unusedcode,naming", 342 | verbose: true, 343 | clearOnClose: false 344 | }; 345 | 346 | // Fake environment 347 | let environment = { 348 | homeDir: "/home/JohnDoe", 349 | workspaceFolders: [ 350 | { 351 | name: "www", 352 | uri: "file://var/www" 353 | } 354 | ] 355 | }; 356 | 357 | // Fake document 358 | let document = { 359 | uri: "test" 360 | }; 361 | 362 | // Stub connection 363 | let connection = {}; 364 | connection.sendDiagnostics = sinon.spy(); 365 | 366 | // Create and configure controller 367 | let controller = new PhpmdController(connection, settings, environment); 368 | controller.setLogger(new NullLogger()); 369 | 370 | // Act 371 | // === 372 | controller.clear(document); 373 | 374 | // Assert 375 | expect((connection.sendDiagnostics).notCalled).to.be.true; 376 | } 377 | }; 378 | -------------------------------------------------------------------------------- /tests/factory/BuildDiagnosticsTaskFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import BuildDiagnosticsTaskFactory from "../../server/factory/BuildDiagnosticsTaskFactory"; 6 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 7 | 8 | @suite("BuildDiagnosticsTask factory") 9 | class BuildDiagnosticsTaskFactoryTest { 10 | 11 | @test("Should create BuildDiagnosticTask instance") 12 | public assertCreate() { 13 | // Arrange 14 | // ======= 15 | // Fake settings 16 | let settings = {}; 17 | 18 | // Create and configure factory instance 19 | let factory = new BuildDiagnosticsTaskFactory(settings); 20 | 21 | // Act 22 | let task = factory.create(); 23 | 24 | // Assert 25 | expect(task).to.be.instanceof(Task); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/factory/ClientConnectionNotifierFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { IConnection } from "vscode-languageserver"; 5 | import ClientConnectionNotifierFactory from "../../server/factory/ClientConnectionNotifierFactory"; 6 | import ClientConnectionNotifier from "../../server/service/notifier/ClientConnectionNotifier"; 7 | 8 | @suite("Client connection notifier factory") 9 | class ClientConnectionNotifierFactoryTest { 10 | 11 | @test("Should create ClientConnectionNotifier instance") 12 | public assertCreate() { 13 | // Arrange 14 | // ======= 15 | // Fake connection 16 | let connection = {}; 17 | 18 | let factory = new ClientConnectionNotifierFactory(); 19 | factory.setConnection(connection); 20 | 21 | // Act 22 | let notifier = factory.create(); 23 | 24 | // Assert 25 | expect(notifier).to.be.instanceof(ClientConnectionNotifier); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/factory/ExecuteProcessTaskFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import ExecuteProcessTaskFactory from "../../server/factory/ExecuteProcessTaskFactory"; 6 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 7 | import IPhpmdEnvironmentModel from "../../server/model/IPhpmdEnvironmentModel"; 8 | import { WorkspaceFolder } from "vscode-languageserver"; 9 | 10 | @suite("ExecuteProcessTask factory") 11 | class ExecuteProcessTaskFactoryTest { 12 | 13 | @test("Should create ExecuteProcessTask instance") 14 | public assertCreate() { 15 | // Arrange 16 | // ======= 17 | // Fake settings 18 | let settings = { 19 | command: "test", 20 | rules: "test", 21 | verbose: true 22 | }; 23 | 24 | // Fake environment 25 | let environment = { 26 | homeDir: "/home/JohnDoe", 27 | workspaceFolders: [ 28 | { 29 | name: "www", 30 | uri: "file://var/www" 31 | } 32 | ] 33 | }; 34 | 35 | // Create and configure factory instance 36 | let factory = new ExecuteProcessTaskFactory(settings, environment); 37 | 38 | // Act 39 | let task = factory.create(); 40 | 41 | // Assert 42 | expect(task).to.be.instanceof(Task); 43 | } 44 | } -------------------------------------------------------------------------------- /tests/factory/NullLoggerFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { IConnection } from "vscode-languageserver"; 5 | import NullLoggerFactory from "../../server/factory/NullLoggerFactory"; 6 | import NullLogger from "../../server/service/logger/NullLogger"; 7 | 8 | @suite("Null logger factory") 9 | class NullLoggerFactoryTest { 10 | 11 | @test("Should create NullLogger instance") 12 | public assertCreate() { 13 | // Arrange 14 | // ======= 15 | // Fake connection 16 | let connection = {}; 17 | 18 | // Create factory instance and configure 19 | let factory = new NullLoggerFactory(); 20 | factory.setConnection(connection); 21 | factory.setVerbose(false); 22 | 23 | // Act 24 | let logger = factory.create(); 25 | 26 | // Assert 27 | expect(logger).to.be.instanceof(NullLogger); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/factory/NullNotifierFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import NullNotifierFactory from "../../server/factory/NullNotifierFactory"; 5 | import NullNotifier from "../../server/service/notifier/NullNotifier"; 6 | 7 | @suite("Null notifier factory") 8 | class NullNotifierFactoryTest { 9 | 10 | @test("Should create NullNotifier instance") 11 | public assertCreate() { 12 | // Arrange 13 | let factory = new NullNotifierFactory(); 14 | 15 | // Act 16 | let notifier = factory.create(); 17 | 18 | // Assert 19 | expect(notifier).to.be.instanceof(NullNotifier); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/factory/ParseTaskFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import ParseTaskFactory from "../../server/factory/ParseTaskFactory"; 6 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 7 | 8 | @suite("ParseTask factory") 9 | class ParseTaskFactoryTest { 10 | 11 | @test("Should create ParseTask instance") 12 | public assertCreate() { 13 | // Arrange 14 | // ======= 15 | // Fake settings 16 | let settings = { 17 | command: "test", 18 | rules: "test", 19 | verbose: true 20 | }; 21 | 22 | // Create and configure factory instance 23 | let factory = new ParseTaskFactory(settings); 24 | 25 | // Act 26 | let task = factory.create(); 27 | 28 | // Assert 29 | expect(task).to.be.instanceof(Task); 30 | } 31 | } -------------------------------------------------------------------------------- /tests/factory/PhpmdControllerFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import { IConnection } from "vscode-languageserver"; 6 | import PhpmdController from "../../server/controller/PhpmdController"; 7 | import PhpmdControllerFactory from "../../server/factory/PhpmdControllerFactory"; 8 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 9 | 10 | @suite("PhpmdController factory") 11 | class PhpmdControllerFactoryTest { 12 | 13 | @test("Should create PhpmdController instance") 14 | public assertCreate() { 15 | // Arrange 16 | // ======= 17 | // Fake connnection 18 | let connection = {}; 19 | 20 | // Fake settings 21 | let settings = {}; 22 | 23 | // Create and configure factory instance 24 | let factory = new PhpmdControllerFactory(); 25 | factory.setConnection(connection); 26 | factory.setSettings(settings); 27 | 28 | // Act 29 | let controller = factory.create(); 30 | 31 | // Assert 32 | expect(controller).to.be.instanceof(PhpmdController); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/factory/PipelineFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { Pipeline } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import { IConnection } from "vscode-languageserver"; 6 | import PipelineFactory from "../../server/factory/PipelineFactory"; 7 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 8 | import IPhpmdEnvironmentModel from "../../server/model/IPhpmdEnvironmentModel"; 9 | 10 | @suite("Pipeline factory") 11 | class PipelineFactoryTest { 12 | 13 | @test("Should create Pipeline instance") 14 | public assertCreate() { 15 | // Arrange 16 | // ======= 17 | // Fake settings 18 | let settings = {}; 19 | 20 | // Fake environment 21 | let environment = {}; 22 | 23 | // Create and configure factory instance 24 | let factory = new PipelineFactory(settings, environment); 25 | 26 | // Act 27 | let pipeline = factory.create(); 28 | 29 | // Assert 30 | expect(pipeline).to.be.instanceof(Pipeline); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/factory/PipelinePayloadFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import PipelinePayloadFactory from "../../server/factory/PipelinePayloadFactory"; 5 | import PipelinePayloadModel from "../../server/model/PipelinePayloadModel"; 6 | 7 | @suite("PipelinePayload factory") 8 | class PipelineFactoryTest { 9 | 10 | @test("Should create PipelinePayload instance") 11 | public assertCreate() { 12 | // Arrange 13 | // ======= 14 | // Fake settings 15 | let uri = "Test"; 16 | 17 | // Create and configure factory instance 18 | let factory = new PipelinePayloadFactory(uri); 19 | 20 | // Act 21 | let pipelinePayload = factory.create(); 22 | 23 | // Assert 24 | expect(pipelinePayload).to.be.instanceof(PipelinePayloadModel); 25 | expect(pipelinePayload.uri).to.equal(uri); 26 | } 27 | 28 | @test("Should update uri on setter") 29 | public assertSetUri() { 30 | // Arrange 31 | // ======= 32 | // Fake settings 33 | let uri = "Test1"; 34 | let uri2 = "Test2"; 35 | 36 | // Create and configure factory instance 37 | let factory = new PipelinePayloadFactory(uri); 38 | factory.setUri(uri2); 39 | 40 | // Act 41 | let pipelinePayload = factory.create(); 42 | 43 | // Assert 44 | expect(pipelinePayload).to.be.instanceof(PipelinePayloadModel); 45 | expect(pipelinePayload.uri).to.equal(uri2); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/factory/RemoteConsoleLoggerFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { IConnection } from "vscode-languageserver"; 5 | import RemoteConsoleLoggerFactory from "../../server/factory/RemoteConsoleLoggerFactory"; 6 | import ILogger from "../../server/service/logger/ILogger"; 7 | import RemoteConsoleLogger from "../../server/service/logger/RemoteConsoleLogger"; 8 | 9 | @suite("RemoteConsoleLogger factory") 10 | class RemoteConsoleLoggerFactoryTest { 11 | 12 | @test("Should create RemoteConsoleLogger instance") 13 | public assertCreate() { 14 | // Arrange 15 | // ======= 16 | // Fake connection 17 | let connection = {}; 18 | connection.console = {}; 19 | 20 | // Create factory instance and configure 21 | let factory = new RemoteConsoleLoggerFactory(); 22 | factory.setConnection(connection); 23 | factory.setVerbose(false); 24 | 25 | // Act 26 | let logger = factory.create(); 27 | 28 | // Assert 29 | expect(logger).to.be.instanceof(RemoteConsoleLogger); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/factory/TestFileTaskFactoryTest.ts: -------------------------------------------------------------------------------- 1 | import { Task } from "@open-sourcerers/j-stillery"; 2 | import { assert, expect } from "chai"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import TestFileTaskFactory from "../../server/factory/TestFileTaskFactory"; 6 | import IPhpmdSettingsModel from "../../server/model/IPhpmdSettingsModel"; 7 | 8 | @suite("TestFileTask factory") 9 | class TestFileTaskFactoryTest { 10 | 11 | @test("Should create TestFileTask instance") 12 | public assertCreate() { 13 | // Arrange 14 | // ======= 15 | // Fake settings 16 | let settings = { 17 | command: "test", 18 | rules: "test", 19 | verbose: true 20 | }; 21 | 22 | // Create and configure factory instance 23 | let factory = new TestFileTaskFactory(settings); 24 | 25 | // Act 26 | let task = factory.create(); 27 | 28 | // Assert 29 | expect(task).to.be.instanceof(Task); 30 | } 31 | } -------------------------------------------------------------------------------- /tests/model/PipelinePayloadModelTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { URI } from "vscode-uri"; 5 | import PipelinePayloadModel from "../../server/model/PipelinePayloadModel"; 6 | 7 | @suite("Pipeline payload model") 8 | class PipelinePayloadModelTest { 9 | 10 | @test("Get path from uri") 11 | public assertPath() { 12 | // Arrange 13 | // ======= 14 | // Fake uri 15 | let uri = "file:///c%3A/Projects/Untitled-2.php"; 16 | 17 | // Model 18 | let model = new PipelinePayloadModel(uri); 19 | 20 | // Act 21 | let path = model.path; 22 | 23 | // Assert 24 | expect(path).to.equal(URI.parse(uri).fsPath); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/service/PhpmdCommandBuilderTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import PhpmdCommandBuilder from "../../server/service/PhpmdCommandBuilder"; 5 | import { URI } from "vscode-uri"; 6 | import { WorkspaceFolder } from "vscode-languageserver"; 7 | 8 | @suite("PhpMD Command Builder") 9 | class PhpmdCommandBuilderTest { 10 | 11 | @test("Should return true if using global php") 12 | public assertUsingGlobalPhp() { 13 | // Arrange 14 | const command = "php /path/to/phpmd.phar"; 15 | const unsafeCommand = ""; 16 | const unsafeCommandEnabled = false; 17 | const workspaceFolders = []; 18 | const homeDir = ""; 19 | 20 | // Act 21 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 22 | 23 | // Assert 24 | expect(commandBuilder.usingGlobalPhp()).to.be.true; 25 | } 26 | 27 | @test("Should return false if not using global php") 28 | public assertNotUsingGlobalPhp() { 29 | // Arrange 30 | const command = "/path/to/php /path/to/phpmd.phar"; 31 | const unsafeCommand = ""; 32 | const unsafeCommandEnabled = false; 33 | const workspaceFolders = []; 34 | const homeDir = ""; 35 | 36 | // Act 37 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 38 | 39 | // Assert 40 | expect(commandBuilder.usingGlobalPhp()).to.be.false; 41 | } 42 | 43 | @test("Should build phpmd version command") 44 | public assertBuildVersionCommand() { 45 | // Arrange 46 | const command = "php /path/to/phpmd.phar"; 47 | const unsafeCommand = ""; 48 | const unsafeCommandEnabled = false; 49 | const workspaceFolders = []; 50 | const homeDir = ""; 51 | 52 | // Act 53 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 54 | 55 | // Assert 56 | expect(commandBuilder.buildPhpmdVersionCommand()).to.equal("php /path/to/phpmd.phar --version"); 57 | } 58 | 59 | @test("Should build phpmd command") 60 | public assertBuildPhpMDCommand() { 61 | // Arrange 62 | const command = "php /path/to/phpmd.phar"; 63 | const unsafeCommand = ""; 64 | const unsafeCommandEnabled = false; 65 | const workspaceFolders = []; 66 | const homeDir = ""; 67 | const options = `"/path/to/file.php" xml "cleancode,codesize,controversial,design,unusedcode,naming"`; 68 | 69 | // Act 70 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 71 | 72 | // Assert 73 | expect(commandBuilder.buildPhpmdCommand(options)) 74 | .to.equal(`php /path/to/phpmd.phar "/path/to/file.php" xml "cleancode,codesize,controversial,design,unusedcode,naming"`); 75 | } 76 | 77 | @test("Should replace unix homedir and build phpmd command") 78 | public assertUnixHomeBuildPhpMDCommand() { 79 | // Arrange 80 | const command = "php /path/to/phpmd.phar"; 81 | const unsafeCommand = ""; 82 | const unsafeCommandEnabled = false; 83 | const workspaceFolders = []; 84 | const homeDir = "/home"; 85 | const options = `"/path/to/file.php" xml "~/phpmd.xml"`; 86 | 87 | // Act 88 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 89 | 90 | // Assert 91 | expect(commandBuilder.buildPhpmdCommand(options)) 92 | .to.equal(`php /path/to/phpmd.phar "/path/to/file.php" xml "/home/phpmd.xml"`); 93 | } 94 | 95 | @test("Should replace windows homedir and build phpmd command") 96 | public assertWindowsHomeBuildPhpMDCommand() { 97 | // Arrange 98 | const command = "php /path/to/phpmd.phar"; 99 | const unsafeCommand = ""; 100 | const unsafeCommandEnabled = false; 101 | const workspaceFolders = []; 102 | const homeDir = "C:\\Users\\Test"; 103 | const options = `"/path/to/file.php" xml "~\\phpmd.xml"`; 104 | 105 | // Act 106 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 107 | 108 | // Assert 109 | expect(commandBuilder.buildPhpmdCommand(options)) 110 | .to.equal(`php /path/to/phpmd.phar "/path/to/file.php" xml "C:\\Users\\Test\\phpmd.xml"`); 111 | } 112 | 113 | @test("Should replace workspace folder and build phpmd command") 114 | public assertWorkspaceFolderBuildPhpMDCommand() { 115 | // Arrange 116 | const command = "php /path/to/phpmd.phar"; 117 | const unsafeCommand = ""; 118 | const unsafeCommandEnabled = false; 119 | const workspaceFolders: WorkspaceFolder[] = [ 120 | { name: "nomatch", uri: "file:///no/match/to/test" }, 121 | { name: "test", uri: "file:///path/to/test" }, 122 | ]; 123 | const homeDir = "/home"; 124 | const options = `"${URI.parse("/path/to/test/file.php").fsPath}" xml "$\{workspaceFolder\}/phpmd.xml"`; 125 | 126 | // Act 127 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 128 | 129 | // Assert 130 | expect(commandBuilder.buildPhpmdCommand(options)) 131 | .to.equal(`php /path/to/phpmd.phar "${URI.parse("/path/to/test/file.php").fsPath}" xml "${URI.parse("file:///path/to/test").fsPath}/phpmd.xml"`); 132 | } 133 | 134 | @test("Should default to first workspace folder if no match found and build phpmd command") 135 | public assertDefaultWorkspaceFolderBuildPhpMDCommand() { 136 | // Arrange 137 | const command = "php /path/to/phpmd.phar"; 138 | const unsafeCommand = ""; 139 | const unsafeCommandEnabled = false; 140 | const workspaceFolders: WorkspaceFolder[] = [ 141 | { name: "nomatch", uri: "file:///no/match/to/test" }, 142 | { name: "test", uri: "file:///path/to/test" }, 143 | ]; 144 | const homeDir = "/home"; 145 | const options = `"${URI.parse("/not/path/to/test/file.php").fsPath}" xml "$\{workspaceFolder\}/phpmd.xml"`; 146 | 147 | // Act 148 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 149 | 150 | // Assert 151 | expect(commandBuilder.buildPhpmdCommand(options)) 152 | .to.equal(`php /path/to/phpmd.phar "${URI.parse("/not/path/to/test/file.php").fsPath}" xml "${URI.parse("file:///no/match/to/test").fsPath}/phpmd.xml"`); 153 | } 154 | 155 | @test("Should use unsafe command if enabled and not empty") 156 | public assertUsingUnsafeCommand() { 157 | // Arrange 158 | const command = "php /path/to/phpmd.phar"; 159 | const unsafeCommand = "php /path/to/unsafe/phpmd.phar"; 160 | const unsafeCommandEnabled = true; 161 | const workspaceFolders = []; 162 | const homeDir = ""; 163 | 164 | // Act 165 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 166 | 167 | // Assert 168 | expect(commandBuilder.buildPhpmdVersionCommand()).to.equal(`${unsafeCommand} --version`); 169 | } 170 | 171 | @test("Should not use unsafe command if not enabled and not empty") 172 | public assertNotUsingDisabledUnsafeCommand() { 173 | // Arrange 174 | const command = "php /path/to/phpmd.phar"; 175 | const unsafeCommand = "php /path/to/unsafe/phpmd.phar"; 176 | const unsafeCommandEnabled = false; 177 | const workspaceFolders = []; 178 | const homeDir = ""; 179 | 180 | // Act 181 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 182 | 183 | // Assert 184 | expect(commandBuilder.buildPhpmdVersionCommand()).to.equal(`${command} --version`); 185 | } 186 | 187 | @test("Should not use unsafe command if enabled and empty") 188 | public assertNotUsingEmptyUnsafeCommand() { 189 | // Arrange 190 | const command = "php /path/to/phpmd.phar"; 191 | const unsafeCommand = ""; 192 | const unsafeCommandEnabled = true; 193 | const workspaceFolders = []; 194 | const homeDir = ""; 195 | 196 | // Act 197 | const commandBuilder = new PhpmdCommandBuilder(command, unsafeCommandEnabled, unsafeCommand, workspaceFolders, homeDir); 198 | 199 | // Assert 200 | expect(commandBuilder.buildPhpmdVersionCommand()).to.equal(`${command} --version`); 201 | } 202 | } -------------------------------------------------------------------------------- /tests/service/PhpmdServiceTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import * as Process from "child_process"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import * as stream from "stream"; 6 | import PhpmdService from "../../server/service/PhpmdService"; 7 | import PhpmdCommandBuilder from "../../server/service/PhpmdCommandBuilder"; 8 | 9 | @suite("PhpMD service") 10 | class ServerTest { 11 | 12 | @test("Should run the command") 13 | public assertRun(done) { 14 | // Arrange 15 | // ======= 16 | // Fake executable 17 | let executable = "testExecutable" 18 | 19 | // Fake commandBuilder 20 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 21 | 22 | // Fake options 23 | let options = "testOptions"; 24 | 25 | // Set encoding spy 26 | let setEncodingSpy = sinon.spy(); 27 | 28 | // On stub 29 | let onStub = sinon.stub(); 30 | onStub.withArgs("data").callsArgWith(1, "Test data"); 31 | onStub.withArgs("close").callsArg(1); 32 | 33 | // Fake process 34 | let process = {}; 35 | process.stdout = {}; 36 | process.stdout.setEncoding = setEncodingSpy; 37 | process.stdout.on = onStub; 38 | 39 | // Executor stub 40 | let executor = sinon.stub(); 41 | executor.returns(process); 42 | 43 | // Act 44 | let service = new PhpmdService(commandBuilderFake); 45 | service.setExecutor(executor); 46 | service.run(options).then((result) => { 47 | // Assert 48 | expect(executor.calledOnce).to.be.true; 49 | expect(executor.calledWithExactly(executable + " " + options)).to.be.true; 50 | expect(result).to.equal("Test data"); 51 | done(); 52 | }); 53 | } 54 | 55 | @test("Should reject the result promise on error") 56 | public assertRejectError(done) { 57 | // Arrange 58 | // ======= 59 | // Fake executable 60 | let executable = "testExecutable"; 61 | 62 | // Fake commandBuilder 63 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 64 | 65 | // Fake options 66 | let options = "testOptions"; 67 | 68 | // Set encoding spy 69 | let setEncodingSpy = sinon.spy(); 70 | 71 | // On stub 72 | let onStub = sinon.stub(); 73 | onStub.withArgs("data").callsArgWith(1, "Test data"); 74 | onStub.withArgs("error").callsArgWith(1, Error("Test error")); 75 | 76 | // Fake process 77 | let process = {}; 78 | process.stdout = {}; 79 | process.stdout.setEncoding = setEncodingSpy; 80 | process.stdout.on = onStub; 81 | 82 | // Executor stub 83 | let executor = sinon.stub(); 84 | executor.returns(process); 85 | 86 | // Act 87 | let service = new PhpmdService(commandBuilderFake); 88 | service.setExecutor(executor); 89 | service.run(options).then(null, (err: Error) => { 90 | // Assert 91 | expect(err.message).to.equal("Test error"); 92 | done(); 93 | }); 94 | } 95 | 96 | @test("Should reject the result promise on empty response") 97 | public assertRejectEmpty(done) { 98 | // Arrange 99 | // ======= 100 | // Fake executable 101 | let executable = "testExecutable"; 102 | 103 | // Fake commandBuilder 104 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 105 | 106 | // Fake options 107 | let options = "testOptions"; 108 | 109 | // Set encoding spy 110 | let setEncodingSpy = sinon.spy(); 111 | 112 | // On stub 113 | let onStub = sinon.stub(); 114 | onStub.withArgs("close").callsArg(1); 115 | 116 | // Fake process 117 | let process = {}; 118 | process.stdout = {}; 119 | process.stdout.setEncoding = setEncodingSpy; 120 | process.stdout.on = onStub; 121 | 122 | // Executor stub 123 | let executor = sinon.stub(); 124 | executor.returns(process); 125 | 126 | // Act 127 | let service = new PhpmdService(commandBuilderFake); 128 | service.setExecutor(executor); 129 | service.run(options).then(null, (err: Error) => { 130 | // Assert 131 | expect(err.message).to.equal("An error occured, no output was received after executing the phpmd command"); 132 | done(); 133 | }); 134 | } 135 | 136 | @test("Should test the phpmd command") 137 | public assertTestCustomCommand(done) { 138 | // Arrange 139 | // ======= 140 | // Fake executable 141 | let executable = "testExecutable"; 142 | 143 | // Fake commandBuilder 144 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 145 | 146 | // Set encoding spy 147 | let setEncodingSpy = sinon.spy(); 148 | 149 | // On stub 150 | let onStub = sinon.stub(); 151 | onStub.withArgs("data").callsArgWith(1, "Test data"); 152 | onStub.withArgs("close").callsArg(1); 153 | 154 | // Fake process 155 | let process = {}; 156 | process.stdout = {}; 157 | process.stdout.setEncoding = setEncodingSpy; 158 | process.stdout.on = onStub; 159 | 160 | // Executor stub 161 | let executor = sinon.stub(); 162 | executor.returns(process); 163 | 164 | // Act 165 | let service = new PhpmdService(commandBuilderFake); 166 | service.setExecutor(executor); 167 | service.testPhpmd().then((result) => { 168 | // Assert 169 | expect(executor.calledOnce).to.be.true; 170 | expect(executor.calledWithExactly(executable + " --version")).to.be.true; 171 | expect(result).to.equal(true); 172 | done(); 173 | }); 174 | } 175 | 176 | @test("Should test the php command") 177 | public assertTestDefaultCommand(done) { 178 | // Arrange 179 | // ======= 180 | // Fake executable 181 | let executable = "php testExecutable"; 182 | 183 | // Fake commandBuilder 184 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 185 | 186 | // Set encoding spy 187 | let setEncodingSpy = sinon.spy(); 188 | 189 | // On stub 190 | let onStub = sinon.stub(); 191 | onStub.withArgs("data").callsArgWith(1, "Test data"); 192 | onStub.withArgs("close").callsArg(1); 193 | 194 | // Fake process 195 | let process = {}; 196 | process.stdout = {}; 197 | process.stdout.setEncoding = setEncodingSpy; 198 | process.stdout.on = onStub; 199 | 200 | // Executor stub 201 | let executor = sinon.stub(); 202 | executor.returns(process); 203 | 204 | // Act 205 | let service = new PhpmdService(commandBuilderFake); 206 | service.setExecutor(executor); 207 | service.testPhpmd().then((result) => { 208 | // Assert 209 | expect(executor.calledTwice).to.be.true; 210 | expect(executor.firstCall.calledWithExactly("php -v")).to.be.true; 211 | expect(executor.secondCall.calledWithExactly(executable + " --version")).to.be.true; 212 | expect(result).to.equal(true); 213 | done(); 214 | }); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /tests/service/logger/NullLoggerTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import NullLogger from "../../../server/service/logger/NullLogger"; 5 | 6 | @suite("NullLogger service") 7 | class NullLoggerTest { 8 | 9 | @test("Should provide a null implementation of ILogger") 10 | public assertImplementILogger() { 11 | // Arrange 12 | let logger = new NullLogger(); 13 | 14 | // Act 15 | logger.setVerbose(true) 16 | .error("error") 17 | .info("info") 18 | .log("log") 19 | .warn("warn"); 20 | 21 | // Assert 22 | expect(logger.getVerbose()).to.be.true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/service/logger/RemoteConsoleLoggerTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { RemoteConsole } from "vscode-languageserver"; 5 | import RemoteConsoleLogger from "../../../server/service/logger/RemoteConsoleLogger"; 6 | 7 | @suite("RemoteConsoleLogger service") 8 | class RemoteConsoleLoggerTest { 9 | 10 | @test("Should get/set verbose flag") 11 | public assertVerbose() { 12 | // Arrange 13 | // ======= 14 | // Fake remote console 15 | let remoteConsole = {}; 16 | 17 | // Create logger 18 | let logger = new RemoteConsoleLogger(remoteConsole); 19 | 20 | // Act 21 | logger.setVerbose(true); 22 | 23 | // Assert 24 | expect(logger.getVerbose()).to.be.true; 25 | } 26 | 27 | @test("Should log error messages to the remote console") 28 | public assertError() { 29 | // Arrange 30 | // ======= 31 | // Spy error method 32 | let errorSpy = sinon.spy(); 33 | 34 | // Fake remote console 35 | let remoteConsole = {}; 36 | remoteConsole.error = errorSpy; 37 | 38 | // Create logger instance 39 | let logger = new RemoteConsoleLogger(remoteConsole); 40 | 41 | // Act 42 | logger.error("error"); // log regular error 43 | logger.error("verbose error", true); // log verbose error 44 | 45 | // Assert 46 | expect(errorSpy.calledOnce).to.be.true; 47 | expect(errorSpy.calledWithExactly("error")).to.be.true; 48 | } 49 | 50 | @test("Should log verbose error messages to the remote console") 51 | public assertErrorVerbose() { 52 | // Arrange 53 | // ======= 54 | // Spy error method 55 | let errorSpy = sinon.spy(); 56 | 57 | // Fake remote console 58 | let remoteConsole = {}; 59 | remoteConsole.error = errorSpy; 60 | 61 | // Create logger instance 62 | let logger = new RemoteConsoleLogger(remoteConsole, true); 63 | 64 | // Act 65 | logger.error("error"); // log regular error 66 | logger.error("verbose error", true); // log verbose error 67 | 68 | // Assert 69 | expect(errorSpy.calledTwice).to.be.true; 70 | expect(errorSpy.calledWithExactly("error")).to.be.true; 71 | expect(errorSpy.calledWithExactly("verbose error")).to.be.true; 72 | } 73 | 74 | @test("Should log warning messages to the remote console") 75 | public assertWarn() { 76 | // Arrange 77 | // ======= 78 | // Spy warn method 79 | let warnSpy = sinon.spy(); 80 | 81 | // Fake remote console 82 | let remoteConsole = {}; 83 | remoteConsole.warn = warnSpy; 84 | 85 | // Create logger instance 86 | let logger = new RemoteConsoleLogger(remoteConsole); 87 | 88 | // Act 89 | logger.warn("warn"); // log regular warning 90 | logger.warn("verbose warn", true); // log verbose warning 91 | 92 | // Assert 93 | expect(warnSpy.calledOnce).to.be.true; 94 | expect(warnSpy.calledWithExactly("warn")).to.be.true; 95 | } 96 | 97 | @test("Should log verbose warning messages to the remote console") 98 | public assertWarnVerbose() { 99 | // Arrange 100 | // ======= 101 | // Spy warn method 102 | let warnSpy = sinon.spy(); 103 | 104 | // Fake remote console 105 | let remoteConsole = {}; 106 | remoteConsole.warn = warnSpy; 107 | 108 | // Create logger instance 109 | let logger = new RemoteConsoleLogger(remoteConsole, true); 110 | 111 | // Act 112 | logger.warn("warn"); // log regular warning 113 | logger.warn("verbose warn", true); // log verbose warning 114 | 115 | // Assert 116 | expect(warnSpy.calledTwice).to.be.true; 117 | expect(warnSpy.calledWithExactly("warn")).to.be.true; 118 | expect(warnSpy.calledWithExactly("verbose warn")).to.be.true; 119 | } 120 | 121 | @test("Should log info messages to the remote console") 122 | public assertInfo() { 123 | // Arrange 124 | // ======= 125 | // Spy info method 126 | let infoSpy = sinon.spy(); 127 | 128 | // Fake remote console 129 | let remoteConsole = {}; 130 | remoteConsole.info = infoSpy; 131 | 132 | // Create logger instance 133 | let logger = new RemoteConsoleLogger(remoteConsole); 134 | 135 | // Act 136 | logger.info("info"); // log regular info message 137 | logger.info("verbose info", true); // log verbose info message 138 | 139 | // Assert 140 | expect(infoSpy.calledOnce).to.be.true; 141 | expect(infoSpy.calledWithExactly("info")).to.be.true; 142 | } 143 | 144 | @test("Should log verbose info messages to the remote console") 145 | public assertInfoVerbose() { 146 | // Arrange 147 | // ======= 148 | // Spy info method 149 | let infoSpy = sinon.spy(); 150 | 151 | // Fake remote console 152 | let remoteConsole = {}; 153 | remoteConsole.info = infoSpy; 154 | 155 | // Create logger instance 156 | let logger = new RemoteConsoleLogger(remoteConsole, true); 157 | 158 | // Act 159 | logger.info("info"); // log regular info message 160 | logger.info("verbose info", true); // log verbose info message 161 | 162 | // Assert 163 | expect(infoSpy.calledTwice).to.be.true; 164 | expect(infoSpy.calledWithExactly("info")).to.be.true; 165 | expect(infoSpy.calledWithExactly("verbose info")).to.be.true; 166 | } 167 | 168 | @test("Should log log messages to the remote console") 169 | public assertLog() { 170 | // Arrange 171 | // ======= 172 | // Spy log method 173 | let logSpy = sinon.spy(); 174 | 175 | // Fake remote console 176 | let remoteConsole = {}; 177 | remoteConsole.log = logSpy; 178 | 179 | // Create logger instance 180 | let logger = new RemoteConsoleLogger(remoteConsole); 181 | 182 | // Act 183 | logger.log("log"); // log regular log message 184 | logger.log("verbose log", true); // log verbose log message 185 | 186 | // Assert 187 | expect(logSpy.calledOnce).to.be.true; 188 | expect(logSpy.calledWithExactly("log")).to.be.true; 189 | } 190 | 191 | @test("Should log verbose log messages to the remote console") 192 | public assertLogVerbose() { 193 | // Arrange 194 | // ======= 195 | // Spy log method 196 | let logSpy = sinon.spy(); 197 | 198 | // Fake remote console 199 | let remoteConsole = {}; 200 | remoteConsole.log = logSpy; 201 | 202 | // Create logger instance 203 | let logger = new RemoteConsoleLogger(remoteConsole, true); 204 | 205 | // Act 206 | logger.log("log"); // log regular log message 207 | logger.log("verbose log", true); // log verbose log message 208 | 209 | // Assert 210 | expect(logSpy.calledTwice).to.be.true; 211 | expect(logSpy.calledWithExactly("log")).to.be.true; 212 | expect(logSpy.calledWithExactly("verbose log")).to.be.true; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /tests/service/notifier/ClientConnectionNotifierTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { IConnection, MessageType, ShowMessageNotification } from "vscode-languageserver"; 5 | import ClientConnectionNotifier from "../../../server/service/notifier/ClientConnectionNotifier"; 6 | 7 | @suite("Client connection notifier") 8 | class ClientConnectionNotifierTest { 9 | 10 | @test("Should send an error notification") 11 | public assertError() { 12 | // Arrange 13 | // ======= 14 | // Send notification spy 15 | let sendNotificationSpy = sinon.spy(); 16 | 17 | // Fake connection 18 | let connection = {}; 19 | connection.sendNotification = sendNotificationSpy; 20 | 21 | // Act 22 | let notifier = new ClientConnectionNotifier(connection); 23 | notifier.error("Test error"); 24 | 25 | // Assert 26 | expect(sendNotificationSpy.calledOnce).to.be.true; 27 | expect(sendNotificationSpy.calledWithExactly(ShowMessageNotification.type, { 28 | message: "Test error", 29 | type: MessageType.Error 30 | })).to.be.true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/service/notifier/NullNotifierTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import NullNotifier from "../../../server/service/notifier/NullNotifier"; 5 | 6 | @suite("Null notifier") 7 | class NullNotifierTest { 8 | 9 | @test("Should not send an error notification") 10 | public assertError() { 11 | // Arrange 12 | // ======= 13 | let notifier = new NullNotifier(); 14 | 15 | // Act 16 | notifier.error("Test error"); 17 | 18 | // Assert 19 | // Nothing to assert here, just validate that the method can run without problems 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/service/pipeline/BuildDiagnosticsStrategyTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import { Diagnostic, DiagnosticSeverity } from "vscode-languageserver"; 5 | import PipelinePayloadModel from "../../../server/model/PipelinePayloadModel"; 6 | import { IPmd, IPmdFileData, IPmdFileMetaData, IPmdViolation, IPmdViolationMetaData } from "../../../server/model/pmd"; 7 | import BuildDiagnosticsStrategy from "../../../server/service/pipeline/BuildDiagnosticsStrategy"; 8 | 9 | @suite("BuildDiagnostics strategy") 10 | class BuildDiagnosticsStrategyTest { 11 | 12 | @test("Should resolve with an array of diagnostic instances") 13 | public assertResolveDiagnostics(done) { 14 | // Arrange 15 | // ======= 16 | // Fake resolve callback 17 | let resolve = (output: PipelinePayloadModel) => { 18 | // Assert 19 | // ====== 20 | expect(output.diagnostics.length).to.equal(1); 21 | expect(output.diagnostics[0].message).to.equal("Test message"); 22 | expect(output.diagnostics[0].range.start.line).to.equal(0); 23 | expect(output.diagnostics[0].range.start.character).to.equal(0); 24 | expect(output.diagnostics[0].range.end.line).to.equal(0); 25 | expect(output.diagnostics[0].range.end.character).to.equal(Number.MAX_VALUE); 26 | expect(output.diagnostics[0].severity).to.equal(DiagnosticSeverity.Error); 27 | expect(output.diagnostics[0].source).to.equal("PHP Mess Detector"); 28 | done(); 29 | }; 30 | 31 | // Fake reject callback 32 | let reject = (reason: any) => { 33 | // Nothing to do here, test will fail because of absence of done 34 | }; 35 | 36 | // Fake input 37 | let input = {}; 38 | input.pmd = {}; 39 | input.pmd.file = []; 40 | input.pmd.file[0] = {}; 41 | input.pmd.file[0].$ = {}; 42 | input.pmd.file[0].$.name = "Test.php"; 43 | input.pmd.file[0].violation = []; 44 | input.pmd.file[0].violation[0] = {}; 45 | input.pmd.file[0].violation[0]._ = "Test message"; 46 | input.pmd.file[0].violation[0].$ = {}; 47 | input.pmd.file[0].violation[0].$.beginline = "1"; 48 | input.pmd.file[0].violation[0].$.class = "TestClass"; 49 | input.pmd.file[0].violation[0].$.endline = "11"; 50 | input.pmd.file[0].violation[0].$.externalInfoUrl = "http://test.com/test"; 51 | input.pmd.file[0].violation[0].$.package = "TestPackage"; 52 | input.pmd.file[0].violation[0].$.priority = "1"; 53 | input.pmd.file[0].violation[0].$.rule = "TestRule"; 54 | input.pmd.file[0].violation[0].$.ruleset = "TestRuleSet"; 55 | input.diagnostics = []; 56 | 57 | // Create and configure strategy instance 58 | let strategy = new BuildDiagnosticsStrategy(); 59 | 60 | // Act 61 | strategy.execute(input, resolve, reject); 62 | } 63 | 64 | @test("Should resolve without diagnostics if pmd empty") 65 | public assertResolveWithoutPmd(done) { 66 | // Arrange 67 | // ======= 68 | // Fake resolve callback 69 | let resolve = (output: PipelinePayloadModel) => { 70 | // Assert 71 | // ====== 72 | expect(output.diagnostics.length).to.equal(0); 73 | done(); 74 | }; 75 | 76 | // Fake reject callback 77 | let reject = (reason: any) => { 78 | // Nothing to do here, test will fail because of absence of done 79 | }; 80 | 81 | // Fake input 82 | let input = {}; 83 | input.pmd = null; 84 | input.diagnostics = []; 85 | 86 | // Create and configure factory instance 87 | let strategy = new BuildDiagnosticsStrategy(); 88 | 89 | // Act 90 | strategy.execute(input, resolve, reject); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/service/pipeline/ExecuteProcessStrategyTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import * as Process from "child_process"; 3 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 4 | import * as sinon from "sinon"; 5 | import PipelineErrorModel from "../../../server/model/PipelineErrorModel"; 6 | import PipelinePayloadModel from "../../../server/model/PipelinePayloadModel"; 7 | import PhpmdService from "../../../server/service/PhpmdService"; 8 | import ExecuteProcessStrategy from "../../../server/service/pipeline/ExecuteProcessStrategy"; 9 | import { URI } from "vscode-uri"; 10 | import PhpmdCommandBuilder from "../../../server/service/PhpmdCommandBuilder"; 11 | 12 | @suite("ExectuteProcess strategy") 13 | class ExecuteProcessStrategyTest { 14 | 15 | @test("Should resolve with an array of diagnostic instances") 16 | public assertResolveExecute(done) { 17 | // Arrange 18 | // ======= 19 | // Fake resolve callback 20 | let resolve = (output: PipelinePayloadModel) => { 21 | // Assert 22 | // ====== 23 | expect(runStub.calledWithExactly(`"${URI.parse("testUri").fsPath}" xml "testRules"`)).to.be.true; 24 | expect(runStub.calledOnce).to.be.true; 25 | expect(output.raw).to.equal("Test data"); 26 | done(); 27 | }; 28 | 29 | // Fake reject callback 30 | let reject = (reason: any) => { 31 | // Nothing to do here, test will fail because of absence of done 32 | }; 33 | 34 | // Fake executable 35 | let executable = "testExecutable"; 36 | 37 | // Fake commandBuilder 38 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 39 | 40 | // Fake rules 41 | let rules = "testRules"; 42 | 43 | // Fake input 44 | let input = new PipelinePayloadModel("testUri"); 45 | 46 | // Run stub 47 | let runStub = sinon.stub(); 48 | runStub.returns(Promise.resolve("Test data")); 49 | 50 | // Fake service 51 | let service = {}; 52 | service.run = runStub; 53 | 54 | // Initialise strategy and configure 55 | let strategy = new ExecuteProcessStrategy(commandBuilderFake, rules); 56 | strategy.setService(service); 57 | 58 | // Act 59 | strategy.execute(input, resolve, reject); 60 | } 61 | 62 | @test("Should reject with a PipelineErrorModel instance") 63 | public assertRejectExecute(done) { 64 | // Arrange 65 | // ======= 66 | // Fake resolve callback 67 | let resolve = (output: PipelinePayloadModel) => { 68 | // Nothing to do here, test will fail because of absence of done 69 | }; 70 | 71 | // Fake reject callback 72 | let reject = (reason: PipelineErrorModel) => { 73 | // Assert 74 | // ====== 75 | expect(runStub.calledWithExactly(`"${URI.parse("testUri").fsPath}" xml "testRules"`)).to.be.true; 76 | expect(reason.error.message).to.equal("Test error"); 77 | expect(reason.silent).to.be.false; 78 | done(); 79 | }; 80 | 81 | // Fake executable 82 | let executable = "testExecutable"; 83 | 84 | // Fake commandBuilder 85 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 86 | 87 | // Fake rules 88 | let rules = "testRules"; 89 | 90 | // Fake input 91 | let input = new PipelinePayloadModel("testUri"); 92 | 93 | // Run stub 94 | let runStub = sinon.stub(); 95 | runStub.returns(Promise.reject(Error("Test error"))); 96 | 97 | // Fake service 98 | let service = {}; 99 | service.run = runStub; 100 | 101 | // Initialise strategy and configure 102 | let strategy = new ExecuteProcessStrategy(commandBuilderFake, rules); 103 | strategy.setService(service); 104 | 105 | // Act 106 | strategy.execute(input, resolve, reject); 107 | } 108 | 109 | @test("Should left trim the xml result") 110 | public assertLtrimXML(done) { 111 | // Arrange 112 | // ======= 113 | // Fake resolve callback 114 | let resolve = (output: PipelinePayloadModel) => { 115 | // Assert 116 | // ====== 117 | expect(output.raw).to.equal("Test data"); 118 | done(); 119 | }; 120 | 121 | // Fake reject callback 122 | let reject = (reason: any) => { 123 | // Nothing to do here, test will fail because of absence of done 124 | }; 125 | 126 | // Fake executable 127 | let executable = "testExecutable"; 128 | 129 | // Fake commandBuilder 130 | let commandBuilderFake: PhpmdCommandBuilder = new PhpmdCommandBuilder(executable, false, "", [], ""); 131 | 132 | // Fake rules 133 | let rules = "testRules"; 134 | 135 | // Fake input 136 | let input = new PipelinePayloadModel("testUri"); 137 | 138 | // Run stub 139 | let runStub = sinon.stub(); 140 | runStub.returns(Promise.resolve("Test ltrimXML Test data")); 141 | 142 | // Fake service 143 | let service = {}; 144 | service.run = runStub; 145 | 146 | // Initialise strategy and configure 147 | let strategy = new ExecuteProcessStrategy(commandBuilderFake, rules); 148 | strategy.setService(service); 149 | 150 | // Act 151 | strategy.execute(input, resolve, reject); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /tests/service/pipeline/ParseStrategyTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import * as Xml2Js from "xml2js"; 5 | import PipelinePayloadModel from "../../../server/model/PipelinePayloadModel"; 6 | import ParseStrategy from "../../../server/service/pipeline/ParseStrategy"; 7 | import NullLogger from "../../../server/service/logger/NullLogger"; 8 | import { match } from "sinon"; 9 | 10 | @suite("Parse strategy") 11 | class ParseStrategyTest { 12 | 13 | @test("Should resolve with a parsed XML") 14 | public assertResolveParse(done) { 15 | // Arrange 16 | // ======= 17 | // Fake resolve callback 18 | let resolve = (output: PipelinePayloadModel | PromiseLike) => { 19 | // Assert 20 | // ====== 21 | expect(input.pmd.file.length).to.equal(1); 22 | expect(input.pmd.file[0].violation.length).to.equal(1); 23 | done(); 24 | }; 25 | 26 | // Fake reject callback 27 | let reject = (reason: any) => { 28 | // Nothing to do here, test will fail because of absence of done 29 | }; 30 | 31 | // Fake input 32 | let input = { path: "/Test.php" }; 33 | input.raw = ""; 34 | input.raw += ""; 35 | input.raw += ""; 36 | input.raw += ""; 37 | input.raw += "The method activityImageAction() contains an exit expression."; 38 | input.raw += ""; 39 | input.raw += ""; 40 | input.raw += ""; 41 | 42 | // Create and configure strategy instance 43 | let strategy = new ParseStrategy(new Xml2Js.Parser(), new NullLogger()); 44 | 45 | // Act 46 | strategy.execute(input, resolve, reject); 47 | } 48 | 49 | @test("Should log XML parse errors and set pmd to null") 50 | public assertLogError(done) { 51 | let resolve = (output: PipelinePayloadModel | PromiseLike) => { 52 | // Assert 53 | // ====== 54 | expect(input.pmd).to.equal(null); 55 | expect(loggerSpy.calledOnce).to.be.true; 56 | expect(loggerSpy.calledOnceWith(match(/^Unable to parse result xml./g))).to.be.true; 57 | done(); 58 | }; 59 | 60 | // Fake reject callback 61 | let reject = (reason: any) => { 62 | // Nothing to do here, test will fail because of absence of done 63 | }; 64 | 65 | // Spy logger 66 | const logger = new NullLogger(); 67 | const loggerSpy = sinon.spy(logger, "log"); 68 | 69 | // Fake input 70 | let input = { path: "/Test.php" }; 71 | input.raw = ""; 72 | input.raw += ""; 73 | input.raw += ""; 74 | input.raw += ""; 75 | input.raw += "The method activityImageAction() contains an exit expression."; 76 | input.raw += ""; 77 | input.raw += ") => { 93 | // Assert 94 | // ====== 95 | expect(input.pmd.file.length).to.equal(1); 96 | expect(input.pmd.file[0].violation.length).to.equal(1); 97 | expect(input.pmd.file[0].$.name).to.equal(input.path); 98 | expect(loggerSpy.notCalled).to.be.true; 99 | done(); 100 | }; 101 | 102 | // Fake reject callback 103 | let reject = (reason: any) => { 104 | // Nothing to do here, test will fail because of absence of done 105 | }; 106 | 107 | // Spy logger 108 | const logger = new NullLogger(); 109 | const loggerSpy = sinon.spy(logger, "log"); 110 | 111 | // Fake input 112 | let input = { path: "/Test&test.php" }; 113 | input.raw = ""; 114 | input.raw += ""; 115 | input.raw += ""; 116 | input.raw += ""; 117 | input.raw += "The method activityImageAction() contains an exit expression."; 118 | input.raw += ""; 119 | input.raw += ""; 120 | input.raw += ""; 121 | 122 | // Create and configure strategy instance 123 | let strategy = new ParseStrategy(new Xml2Js.Parser(), logger); 124 | 125 | // Act 126 | strategy.execute(input, resolve, reject); 127 | } 128 | 129 | @test("Should trim windows drive when escaping XML special chars") 130 | public assertTrimPath(done) { 131 | // Arrange 132 | // ======= 133 | // Fake resolve callback 134 | let resolve = (output: PipelinePayloadModel | PromiseLike) => { 135 | // Assert 136 | // ====== 137 | expect(input.pmd.file.length).to.equal(1); 138 | expect(input.pmd.file[0].violation.length).to.equal(1); 139 | expect(input.pmd.file[0].$.name).to.equal(input.path); 140 | expect(loggerSpy.notCalled).to.be.true; 141 | done(); 142 | }; 143 | 144 | // Fake reject callback 145 | let reject = (reason: any) => { 146 | // Nothing to do here, test will fail because of absence of done 147 | }; 148 | 149 | // Spy logger 150 | const logger = new NullLogger(); 151 | const loggerSpy = sinon.spy(logger, "log"); 152 | 153 | // Fake input 154 | let input = { path: "c:\\Test&test.php" }; 155 | input.raw = ""; 156 | input.raw += ""; 157 | input.raw += ""; 158 | input.raw += ""; 159 | input.raw += "The method activityImageAction() contains an exit expression."; 160 | input.raw += ""; 161 | input.raw += ""; 162 | input.raw += ""; 163 | 164 | // Create and configure strategy instance 165 | let strategy = new ParseStrategy(new Xml2Js.Parser(), logger); 166 | 167 | // Act 168 | strategy.execute(input, resolve, reject); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/service/pipeline/TestFileStrategyTest.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from "chai"; 2 | import { only, skip, slow, suite, test, timeout } from "@testdeck/mocha"; 3 | import * as sinon from "sinon"; 4 | import PipelineErrorModel from "../../../server/model/PipelineErrorModel"; 5 | import PipelinePayloadModel from "../../../server/model/PipelinePayloadModel"; 6 | import TestFileStrategy from "../../../server/service/pipeline/TestFileStrategy"; 7 | 8 | @suite("Test file strategy") 9 | class TestFileStrategyTest { 10 | 11 | @test("Should resolve if the file is readable") 12 | public assertResolveTest(done) { 13 | // Arrange 14 | // ======= 15 | // Fake resolve callback 16 | let resolve = (output: PipelinePayloadModel) => { 17 | // Assert 18 | // ====== 19 | expect(output).to.equal(input); 20 | done(); 21 | }; 22 | 23 | // Fake reject callback 24 | let reject = (reason: any) => { 25 | // Nothing to do here, test will fail because of absence of done 26 | }; 27 | 28 | // Fake input 29 | let input = {}; 30 | input.uri = "test.php"; 31 | 32 | // Readfile stub 33 | let readfile = sinon.stub(); 34 | readfile.callsArgWith(2, null, input); 35 | 36 | // Create and configure strategy instance 37 | let strategy = new TestFileStrategy(readfile); 38 | 39 | // Act 40 | strategy.execute(input, resolve, reject); 41 | } 42 | 43 | @test("Should reject if the file is not readable") 44 | public assertRejectTest(done) { 45 | // Arrange 46 | // ======= 47 | // Fake resolve callback 48 | let resolve = (output: PipelinePayloadModel) => { 49 | // Nothing to do here, test will fail because of absence of done 50 | }; 51 | 52 | // Fake reject callback 53 | let reject = (reason: PipelineErrorModel) => { 54 | // Assert 55 | // ====== 56 | expect(reason.error.message).to.equal("Test error"); 57 | expect(reason.silent).to.be.true; 58 | done(); 59 | }; 60 | 61 | // Fake input 62 | let input = {}; 63 | input.uri = "test.php"; 64 | 65 | // Readfile stub 66 | let readfile = sinon.stub(); 67 | readfile.callsArgWith(2, Error("Test error"), input); 68 | 69 | // Create and configure strategy instance 70 | let strategy = new TestFileStrategy(readfile); 71 | 72 | // Act 73 | strategy.execute(input, resolve, reject); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "lib": [ "es2016" ], 8 | "outDir": "./out", 9 | "experimentalDecorators": true 10 | }, 11 | "include": [ 12 | "**/*.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "out", 17 | "tests" 18 | ] 19 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "trailing-comma": [ 5 | false 6 | ] 7 | } 8 | } --------------------------------------------------------------------------------