diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 438e6d9..8b382f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: - name: Setup NodeJS # for JS and TS analyzer uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - uses: jcs090218/setup-emacs@master with: diff --git a/Makefile b/Makefile index 7caf549..df838cf 100644 --- a/Makefile +++ b/Makefile @@ -3,11 +3,11 @@ SHELL := /usr/bin/env bash EMACS ?= emacs EASK ?= eask -TEST-FILES := $(shell ls test/*.el) +TEST-FILES := $(shell ls test/*-test.el) -.PHONY: clean checkdoc lint package install compile test +.PHONY: clean checkdoc lint package install compile download-sonarlint test -ci: clean package install compile test +ci: clean package install compile download-sonarlint test package: @echo "Packaging..." @@ -21,6 +21,10 @@ compile: @echo "Compiling..." $(EASK) compile +download-sonarlint: + @echo "Downloading SonarLint..." + $(EASK) eval '(progn (require (quote lsp-sonarlint)) (lsp-sonarlint-download))' + test: @echo "Testing..." $(EASK) test ert $(TEST-FILES) diff --git a/README.md b/README.md index b7c5bc3..dc8ae19 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,54 @@ Click [here](https://github.com/SonarSource/sonarlint-vscode/blob/master/telemet * [treemacs](https://github.com/Alexander-Miller/treemacs) : Project viewer. * [lsp-treemacs](https://github.com/emacs-lsp/lsp-treemacs) : `lsp-mode` GUI controls implemented using treemacs. +## Development + +### Prerequisites + +You will need `make` and [`eask`](https://emacs-eask.github.io/) to run `lsp-sonarlint` tests. +See also (Requirements)(#requirements) section. + +If you do not have `eask` installed, you can install it locally with: + +``` shell +npm install @emacs-eask/cli +export EASK="$PWD/node_modules/@emacs-eask/cli/eask" +``` + +Or globally with: + +``` shell +npm install -g @emacs-eask/cli +``` + +### Testing + +We use [Emacs ERT](https://www.gnu.org/software/emacs/manual/html_node/ert/) for testing. +You can run tests with: + +``` shell +make package +make install +make compile +make download-sonarlint +make test +``` + +#### Interactive Testing + +You can also run the tests one-by-one interactively. + +Open a test file from tests/*.el in Emacs. +Evaluate the file contents (`eval-buffer`) to load test definitions into the session. +Run `ert` command to run all or selected tests. +To run the integration tests, you will have to shut down all your lsp workspaces +to ensure consistent starting state. + +Check out `*lsp-log*`, `*lsp-log: sonarlint:NNNNNNNN*`, `*sonarlint*`, `*sonarlint:stderr*` +for logs when troubleshooting a test. + +You can start with test/trivial-test.el to check that your testing harness works. + ## Contributions Contributions are very much welcome. diff --git a/fixtures/compdb/dir1/1.cpp b/fixtures/compdb/dir1/1.cpp new file mode 100644 index 0000000..6d1a0d4 --- /dev/null +++ b/fixtures/compdb/dir1/1.cpp @@ -0,0 +1 @@ +int x; diff --git a/fixtures/compdb/dir1/compile_commands.json b/fixtures/compdb/dir1/compile_commands.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/fixtures/compdb/dir1/compile_commands.json @@ -0,0 +1 @@ +{} diff --git a/fixtures/compdb/dir1/dir11/11.cpp b/fixtures/compdb/dir1/dir11/11.cpp new file mode 100644 index 0000000..6d1a0d4 --- /dev/null +++ b/fixtures/compdb/dir1/dir11/11.cpp @@ -0,0 +1 @@ +int x; diff --git a/fixtures/compdb/dir1/dir11/compile_commands.json b/fixtures/compdb/dir1/dir11/compile_commands.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/fixtures/compdb/dir1/dir11/compile_commands.json @@ -0,0 +1 @@ +{} diff --git a/fixtures/compdb/dir1/dir12/12.cpp b/fixtures/compdb/dir1/dir12/12.cpp new file mode 100644 index 0000000..6d1a0d4 --- /dev/null +++ b/fixtures/compdb/dir1/dir12/12.cpp @@ -0,0 +1 @@ +int x; diff --git a/fixtures/compdb/dir1/dir12/compile_commands.yml b/fixtures/compdb/dir1/dir12/compile_commands.yml new file mode 100644 index 0000000..e69de29 diff --git a/fixtures/compdb/dir1/dir12/dir121/121.cpp b/fixtures/compdb/dir1/dir12/dir121/121.cpp new file mode 100644 index 0000000..6d1a0d4 --- /dev/null +++ b/fixtures/compdb/dir1/dir12/dir121/121.cpp @@ -0,0 +1 @@ +int x; diff --git a/fixtures/compdb/no-compdb/no-compdb.cpp b/fixtures/compdb/no-compdb/no-compdb.cpp new file mode 100644 index 0000000..6d1a0d4 --- /dev/null +++ b/fixtures/compdb/no-compdb/no-compdb.cpp @@ -0,0 +1 @@ +int x; diff --git a/fixtures/compile_commands.json b/fixtures/cpp/compile_commands.json similarity index 100% rename from fixtures/compile_commands.json rename to fixtures/cpp/compile_commands.json diff --git a/fixtures/sample.cpp b/fixtures/cpp/sample.cpp similarity index 100% rename from fixtures/sample.cpp rename to fixtures/cpp/sample.cpp diff --git a/lsp-sonarlint.el b/lsp-sonarlint.el index 5a0659e..ca5ebaf 100644 --- a/lsp-sonarlint.el +++ b/lsp-sonarlint.el @@ -87,42 +87,77 @@ If absent, it will be downloaded from github and unzipped there." :group 'lsp-sonarlint :type 'file) -(defcustom lsp-sonarlint-disable-telemetry t +(defcustom-lsp lsp-sonarlint-disable-telemetry t "Disable sending anonymous usage statistics to SonarSource. To see a sample of the data that are collected https://github.com/SonarSource/sonarlint-vscode/blob/master/telemetry-sample.md." :group 'lsp-sonarlint - :type 'boolean) + :type 'boolean + :lsp-path "sonarlint.disableTelemetry") -(defcustom lsp-sonarlint-test-file-pattern "{**/test/**,**/*test*,**/*Test*}" +(defcustom-lsp lsp-sonarlint-test-file-pattern "{**/test/**,**/*test*,**/*Test*}" "Files whose name match java global are considered as test files by analyzers. Most rules are not evaluated on test files. Example: `{**/test/**,**/*test*,**/*Test*}`" :group 'lsp-sonarlint - :type 'string) + :type 'string + :lsp-path "sonarlint.testFilePattern") -(defcustom lsp-sonarlint-show-analyzer-logs nil +(defcustom-lsp lsp-sonarlint-show-analyzer-logs nil "Show analyzer's logs in the SonarLint output." :group 'lsp-sonarlint - :type 'boolean) + :type 'boolean + :lsp-path "sonarlint.output.showAnalyzerLogs") -(defcustom lsp-sonarlint-verbose-logs nil +(defcustom-lsp lsp-sonarlint-verbose-logs nil "Enable verbose logging of the SonarLint language server." :group 'lsp-sonarlint - :type 'boolean) + :type 'boolean + :lsp-path "sonarlint.output.verboseLogs") -(defcustom lsp-sonarlint-vmargs "" +(defcustom-lsp lsp-sonarlint-vmargs "" "Extra JVM arguments used to launch the SonarLint LSP. e.g. `-Xmx1024m`." :group 'lsp-sonarlint - :type 'string) - -(defcustom lsp-sonarlint-cfamily-compile-commands-path - "${workspaceFolder}/compile_commands.json" - "Location of the compile_commands.json file. -It is needed in C/C++ to provide your compilation options." + :type 'string + :lsp-path "sonarlint.ls.vmargs") + + (defun lsp-sonarlint--find-file-in-parent-folders (fname) + "Find the closest FNAME in a buffer folder or one of its parents. + Traverse the parent folders from narrow to wide (/a/b/c, /a/b, /a, /). + If none of them contains FNAME, return nil." + (let ((dir (file-name-directory (expand-file-name (buffer-file-name)))) + (found-file nil)) + (while (not (or found-file (string-empty-p dir) (string-equal dir "/"))) + (let ((potential-file (concat dir fname))) + (if (file-exists-p potential-file) + (setq found-file potential-file) + (setq dir (file-name-directory (directory-file-name dir)))))) + found-file)) + +(defcustom-lsp lsp-sonarlint-path-to-compile-commands "" + "Path to the compilation DB - the compile_commands.json file." + :type 'path :group 'lsp-sonarlint - :type 'file) + :lsp-path "sonarlint.pathToCompileCommands") + + (defun lsp-sonarlint--get-compile-commands () + "Find compile_commands.json in the parent dir or ask user." + (or (lsp-sonarlint--find-file-in-parent-folders "compile_commands.json") + (read-file-name "Provide path to compile_commands.json for this project: "))) + + (defun lsp-sonarlint--set-compile-commands (_workspace _params) + "Find compile_commands.json and set it for the workspace. + As a side effect it will also send the found path to the SonarLint server." + (let ((fname (lsp-sonarlint--get-compile-commands))) + (unless (string-empty-p fname) + (message "Using compilation database from %s." fname) + (custom-set-variables `(lsp-sonarlint-path-to-compile-commands ,(expand-file-name fname)))))) + +(defun lsp-sonarlint-set-compile-commands (fname) + "Set FNAME as the path to the compile_commands.json file for current session." + (interactive "fCompilation DB path: ") + (custom-set-variables `(lsp-sonarlint-path-to-compile-commands ,(expand-file-name fname)))) (defconst lsp-sonarlint-go-doc-url "https://www.sonarsource.com/go/" "Documentation sonarsource URL.") @@ -321,14 +356,6 @@ See `lsp-sonarlint-analyze-folder' to see which files are ignored." (defvar lsp-sonarlint--action-handlers '()) -(lsp-register-custom-settings - '(("sonarlint.disableTelemetry" lsp-sonarlint-disable-telemetry) - ("sonarlint.testFilePattern" lsp-sonarlint-test-file-pattern) - ("sonarlint.pathToCompileCommands" lsp-sonarlint-cfamily-compile-commands-path) - ("sonarlint.output.showAnalyzerLogs" lsp-sonarlint-show-analyzer-logs) - ("sonarlint.output.verboseLogs" lsp-sonarlint-verbose-logs) - ("sonarlint.ls.vmargs" lsp-sonarlint-vmargs))) - (defvar lsp-sonarlint--request-handlers (lsp-ht ;; Check whether the file should be analyzed or not @@ -380,8 +407,7 @@ See REQUEST-HANDLERS in lsp--client in lsp-mode." ("sonarlint/readyForTests" #'ignore) ;; Sent by cfamily for analysis of C/C++ files. Sonar requires your ;; build commands specified in a compile_commands.json - ("sonarlint/needCompilationDatabase" (lambda(&rest _) - (warn "Sonar could not find your compile_commands.json, please check `lsp-sonarlint-cfamily-compile-commands-path'"))) + ("sonarlint/needCompilationDatabase" #'lsp-sonarlint--set-compile-commands) ;; This is probably just to raise awareness of the new kind of issues: ;; secrets. That'd be too booring to implement. Hopefully, the user is ;; paying attention and will notice anyway. @@ -398,18 +424,19 @@ See NOTIFICATION-HANDLERS in lsp--client in lsp-mode.") :priority -1 :request-handlers lsp-sonarlint--request-handlers :notification-handlers lsp-sonarlint--notification-handlers - :multi-root t + :multi-root nil :add-on? t :server-id 'sonarlint :action-handlers (ht<-alist lsp-sonarlint--action-handlers) :initialization-options (lambda () - (list - :productKey "emacs" - :productName "Emacs")) + (list + :productKey "emacs" + :productName "Emacs")) :initialized-fn (lambda (workspace) (with-lsp-workspace workspace (lsp--set-configuration - (lsp-configuration-section "sonarlint")))))) + (lsp-configuration-section "sonarlint")))) + :synchronize-sections '("sonarlint"))) (provide 'lsp-sonarlint) ;;; lsp-sonarlint.el ends here diff --git a/test/lsp-sonarlint-cpp-test.el b/test/lsp-sonarlint-cpp-test.el new file mode 100644 index 0000000..f87533a --- /dev/null +++ b/test/lsp-sonarlint-cpp-test.el @@ -0,0 +1,53 @@ +;;; lsp-sonarlint-cpp-test.el --- CFamily analyzer-specific tests for Sonarlint LSP client -*- lexical-binding: t; -*- +;;; +;; Author: Arseniy Zaostrovnykh +;; Created: 11 July 2023 +;; License: GPL-3.0-or-later +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: +;; Tests for the C++-specific integration of the LSP mode and SonarLint language server. + +;;; Code: + +(require 'lsp-mode) +(require 'lsp-sonarlint) +(load-file (expand-file-name "lsp-sonarlint-test-utils.el" + (file-name-directory (or load-file-name (buffer-file-name))))) + +(defun lsp-sonarlint--get-compdb-for-sample-file (cpp-file) + "Relative to fixtures folder, get compile_commands.json for CPP-FILE." + (let ((buf (find-file-noselect (lsp-sonarlint-sample-file cpp-file)))) + (with-current-buffer buf + (file-relative-name (lsp-sonarlint--get-compile-commands) + (lsp-sonarlint-fixtures-dir))))) + +(ert-deftest lsp-sonarlint--get-compile-commands-test () + (should (equal (lsp-sonarlint--get-compdb-for-sample-file "compdb/dir1/1.cpp") + "compdb/dir1/compile_commands.json")) + (should (equal (lsp-sonarlint--get-compdb-for-sample-file "compdb/dir1/dir11/11.cpp") + "compdb/dir1/dir11/compile_commands.json")) + (should (equal (lsp-sonarlint--get-compdb-for-sample-file "compdb/dir1/dir12/12.cpp") + "compdb/dir1/compile_commands.json")) + (should (equal (lsp-sonarlint--get-compdb-for-sample-file "compdb/dir1/dir12/dir121/121.cpp") + "compdb/dir1/compile_commands.json"))) + +(ert-deftest lsp-sonarlint--get-compile-commands-interactive-test () + (cl-letf (((symbol-function 'read-file-name) (lambda (_prompt) + (concat (lsp-sonarlint-fixtures-dir) "my_db.json")))) + (should (equal (lsp-sonarlint--get-compdb-for-sample-file "compdb/no-compdb/no-compdb.cpp") + "my_db.json")))) + +;;; lsp-sonarlint-cpp-test.el ends here diff --git a/test/lsp-sonarlint-integration-test.el b/test/lsp-sonarlint-integration-test.el index 722350a..d7472dc 100644 --- a/test/lsp-sonarlint-integration-test.el +++ b/test/lsp-sonarlint-integration-test.el @@ -17,6 +17,7 @@ ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see . +;; Package-Requires: ((emacs "27.2")) ;;; Commentary: ;; Tests for the integration of the LSP mode and SonarLint language server @@ -26,6 +27,17 @@ (require 'lsp-mode) (require 'lsp-sonarlint) +(load-file (expand-file-name "lsp-sonarlint-test-utils.el" + (file-name-directory (or load-file-name (buffer-file-name))))) + +(ert-deftest lsp-sonarlint-plugin-downloaded () + "Check whether you have downloaded SonarLint. + +This is a prerequisite for all the integration tests. If this +test fails, you need to download the SonarLint plugin using + +make download-sonarlint" + (should (file-exists-p (concat lsp-sonarlint-download-dir "/extension/server/sonarlint-ls.jar")))) (defun lsp-sonarlint--wait-for (predicate hook timeout) "Register PREDICATE to run on HOOK, and wait until it returns t. @@ -71,7 +83,6 @@ only works for specific textDocument/didOpen:languageId." (lsp-enable-snippet nil) received-warnings) (let ((buf (find-file-noselect file)) - (lsp-sonarlint-plugin-autodownload t) (diagnostics-updated nil) (register-warning (lambda (&rest w) (when (equal (car w) 'lsp-mode) (push (cadr w) received-warnings))))) @@ -101,17 +112,6 @@ only works for specific textDocument/didOpen:languageId." (sort (mapcar (lambda (issue) (gethash "code" issue)) issues) #'string-lessp)) -(defun lsp-sonarlint--fixtures-dir () - "Directory of the test fixtures for these tests." - (concat - (file-name-directory - (directory-file-name (file-name-directory (symbol-file #'lsp-sonarlint--fixtures-dir)))) - "fixtures/")) - -(defun lsp-sonarlint--sample-file (fname) - "Get the full path of the sample file FNAME." - (concat (lsp-sonarlint--fixtures-dir) fname)) - (defun lsp-sonarlint--get-all-issue-codes (sample-filename &optional major-mode) "Get all SonarLint issue-codes for given SAMPLE-FILENAME. This functions takes some time to wait for the LSP mode to init @@ -120,7 +120,7 @@ MAJOR-MODE specifies the major mode enabled to trigger the analysis. Some analyzers like cfamily require specific major-modes. If nil, use python-mode by default." (lsp-sonarlint--exec-with-diags - (lsp-sonarlint--sample-file sample-filename) + (lsp-sonarlint-sample-file sample-filename) (lambda (diags) (lsp-sonarlint--get-codes-of-issues diags)) (if major-mode major-mode 'python-mode))) @@ -176,11 +176,11 @@ If nil, use python-mode by default." (ert-deftest lsp-sonarlint-c++-reports-issues () "Check that LSP can get go SonarLint issues for a C++ file." - (should (equal (lsp-sonarlint--get-all-issue-codes "sample.cpp" 'c++-mode) + (should (equal (lsp-sonarlint--get-all-issue-codes "cpp/sample.cpp" 'c++-mode) '("cpp:S995")))) (defun lsp-sonarlint--find-descr-action-at-point () - "Find the 'get rule description' code action for the issue at point." + "Find the `get rule description' code action for the issue at point." (seq-find (lambda (action) (string-match-p "description" (gethash "title" action))) (lsp-code-actions-at-point))) @@ -208,24 +208,27 @@ If nil, use python-mode by default." (ert-deftest lsp-sonarlint-display-rule-descr-test () "Check whether you can display rule description for a SonarLint issue." (lsp-sonarlint--exec-with-diags - (lsp-sonarlint--sample-file "sample.py") + (lsp-sonarlint-sample-file "sample.py") (lambda (diags) (lsp-sonarlint--go-to-first-diag diags) (let ((descr-action (lsp-sonarlint--find-descr-action-at-point))) (let ((description-opened nil)) - (cl-flet ((check-opened-buffer - (buf) - (when (lsp-sonarlint--buf-has-rule-descr-p buf) - (setq description-opened t)))) + (cl-flet ((check-opened-buffer (buf) + (when (lsp-sonarlint--buf-has-rule-descr-p buf) + (setq description-opened t)))) (unwind-protect (progn (advice-add 'shr-render-buffer :before #'check-opened-buffer) - (sit-for 1) - (lsp-execute-code-action descr-action) (with-timeout (8 (error "Timeout waiting for rule description")) (while (not description-opened) + ;; Repeat the request multiple times because SonarLint + ;; might get distracted with other requests and "forget" to + ;; respond + (lsp-execute-code-action descr-action) (message "still waiting") - (sit-for 0.1))) + (sit-for 0.3))) (should description-opened)) (advice-remove 'shr-render-buffer #'check-opened-buffer)))))) 'python-mode)) + +;;; integration.el ends here diff --git a/test/lsp-sonarlint-test-utils.el b/test/lsp-sonarlint-test-utils.el new file mode 100644 index 0000000..da62eaf --- /dev/null +++ b/test/lsp-sonarlint-test-utils.el @@ -0,0 +1,38 @@ +;;; integration.el --- Integration tests for Sonarlint LSP client -*- lexical-binding: t; -*- +;;; +;; Author: Arseniy Zaostrovnykh +;; Created: 11 July 2023 +;; License: GPL-3.0-or-later +;; +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: +;; Utility functions for the LSP SonarLint tests. + +;;; Code: + +(defun lsp-sonarlint-fixtures-dir () + "Directory of the test fixtures for these tests." + (concat + (file-name-directory + (directory-file-name (file-name-directory (symbol-file #'lsp-sonarlint-fixtures-dir)))) + "fixtures/")) + +(defun lsp-sonarlint-sample-file (fname) + "Get the full path of the sample file FNAME." + (concat (lsp-sonarlint-fixtures-dir) fname)) + +(provide 'lsp-sonarlint-test-utils) + +;;; lsp-sonarlint-test-utils.el ends here