;;; flycheck-clang-tidy.el --- Flycheck syntax checker using clang-tidy -*- lexical-binding:t -*- ;; Author: Sebastian Nagel ;; Maintainer: tastytea ;; URL: https://github.com/ch1bo/flycheck-clang-tidy ;; Keywords: convenience languages tools ;; Package-Version: 20191030.814 ;; Package-X-Original-Version: 0.3.0 ;; Package-Requires: ((flycheck "0.30")) ;; This file is NOT part of GNU Emacs. ;; See LICENSE ;;; Commentary: ;; Adds a Flycheck syntax checker for C/C++ based on clang-tidy. ;;; Usage: ;; (eval-after-load 'flycheck ;; '(add-hook 'flycheck-mode-hook #'flycheck-clang-tidy-setup)) ;;; Code: (require 'flycheck) (require 'dom) ;; To keep variable names consistent. (defvaralias 'flycheck-clang-tidy-executable 'flycheck-c/c++-clang-tidy-executable) (flycheck-def-config-file-var flycheck-clang-tidy c/c++-clang-tidy ".clang-tidy" :type 'string :safe #'stringp) (flycheck-def-option-var flycheck-clang-tidy-build-path "build" c/c++-clang-tidy "Build path to read a compile command database. For example, it can be a CMake build directory in which a file named compile_commands.json exists (use -DCMAKE_EXPORT_COMPILE_COMMANDS=ON CMake option to get this output)." :type 'string :safe #'stringp) (flycheck-def-option-var flycheck-clang-tidy-extra-options nil c/c++-clang-tidy "Extra options to pass to clang-tidy. Set to `nil' to disable." :type 'string :safe #'stringp) (defun flycheck-clang-tidy-find-project-root (checker) "Find the project root for CHECKER using Projectile, vc or the .clang-tidy file." (let ((project-root nil)) (if (member 'projectile-mode minor-mode-list) (setq project-root (projectile-project-root))) (unless project-root (setq project-root (vc-root-dir))) (unless project-root (let ((config_file_location (flycheck-locate-config-file flycheck-clang-tidy checker))) (if config_file_location (setq project-root (file-name-directory config_file_location))))) (unless project-root (message "Could not determine project root, trying current directory.") (setq project-root (flycheck-clang-tidy-current-source-dir))) project-root)) (defun flycheck-clang-tidy-current-source-dir () "Directory of current source file." (file-name-directory (buffer-file-name))) (defun flycheck-clang-tidy-get-config () "Find and read .clang-tidy." (let ((config-file (flycheck-locate-config-file flycheck-clang-tidy 0))) (when config-file (with-temp-buffer (insert-file-contents config-file) (buffer-string))))) (defun flycheck-clang-tidy--skip-http-headers () "Position point just after HTTP headers." (re-search-forward "^$")) (defun flycheck-clang-tidy--narrow-to-http-body () "Narrow the current buffer to contain the body of an HTTP response." (flycheck-clang-tidy--skip-http-headers) (narrow-to-region (point) (point-max))) (defun flycheck-clang-tidy--decode-region-as-utf8 (start end) "Decode a region from START to END in UTF-8." (condition-case nil (decode-coding-region start end 'utf-8) (coding-system-error nil))) (defun flycheck-clang-tidy--remove-crlf () "Remove carriage return and line feeds from the current buffer." (save-excursion (while (re-search-forward "\r$" nil t) (replace-match "" t t)))) (defun flycheck-clang-tidy--extract-relevant-doc-section () "Extract the parts of the LLVM clang-tidy documentation that are relevant. This function assumes that the current buffer contains the result of browsing 'clang.llvm.org', as returned by `url-retrieve'. More concretely, this function returns the main
element with class 'section', and also removes 'headerlinks'." (goto-char (point-min)) (flycheck-clang-tidy--narrow-to-http-body) (flycheck-clang-tidy--decode-region-as-utf8 (point-min) (point-max)) (flycheck-clang-tidy--remove-crlf) (let* ((dom (libxml-parse-html-region (point-min) (point-max))) (section (dom-by-class dom "section"))) (dolist (headerlink (dom-by-class section "headerlink")) (dom-remove-node section headerlink)) section)) (defun flycheck-clang-tidy--explain-error (explanation &rest args) "Explain an error in the Flycheck error explanation buffer using EXPLANATION. EXPLANATION is a function with optional ARGS that, when evaluated, inserts the content in the appropriate Flycheck buffer." (with-current-buffer flycheck-explain-error-buffer (let ((inhibit-read-only t) (inhibit-modification-hooks t)) (erase-buffer) (apply explanation args) (goto-char (point-min))))) (defun flycheck-clang-tidy--show-documentation (error-id) "Show clang-tidy documentation about ERROR-ID. Information comes from the clang.llvm.org website." (url-retrieve (format "https://clang.llvm.org/extra/clang-tidy/checks/%s.html" error-id) (lambda (status) (if-let ((error-status (plist-get status :error))) (flycheck-clang-tidy--explain-error #'insert (format "Error accessing clang-tidy documentation: %s" (error-message-string error-status))) (let ((doc-contents (flycheck-clang-tidy--extract-relevant-doc-section))) (flycheck-clang-tidy--explain-error #'shr-insert-document doc-contents))))) "Loading documentation...") (defun flycheck-clang-tidy-error-explainer (error) "Explain a clang-tidy ERROR by scraping documentation from llvm.org." (unless (fboundp 'libxml-parse-html-region) (error "This function requires Emacs to be compiled with libxml2")) (if-let (err-message (flycheck-error-message error)) (if-let (((string-match "\\[\\(.*\\)\\]" err-message)) (clang-tidy-error-id (match-string 1 err-message))) (condition-case err (flycheck-clang-tidy--show-documentation clang-tidy-error-id) (error (format "Error accessing clang-tidy documentation: %s" (error-message-string err)))) (error "The clang-tidy error message does not contain an [error-id]")) (error "Flycheck error does not contain an error message"))) (flycheck-define-checker c/c++-clang-tidy "A C/C++ syntax checker using clang-tidy. See URL `https://github.com/ch1bo/flycheck-clang-tidy'." :command ("clang-tidy" (option "-p" flycheck-clang-tidy-build-path) (eval (concat "-extra-arg=-I" (flycheck-clang-tidy-current-source-dir))) (eval (concat "-config=" (flycheck-clang-tidy-get-config))) (eval flycheck-clang-tidy-extra-options) source) :error-patterns ((error line-start (file-name) ":" line ":" column ": error: " (message) line-end) (warning line-start (file-name) ":" line ":" column ": warning: " (message) line-end) (info line-start (file-name) ":" line ":" column ": note: " (message) line-end)) :modes (c-mode c++-mode) :working-directory flycheck-clang-tidy-find-project-root :error-explainer flycheck-clang-tidy-error-explainer :predicate (lambda () (buffer-file-name)) :next-checkers ((error . c/c++-cppcheck))) ;;;###autoload (defun flycheck-clang-tidy-setup () "Setup Flycheck clang-tidy." (add-to-list 'flycheck-checkers 'c/c++-clang-tidy)) (provide 'flycheck-clang-tidy) ;;; flycheck-clang-tidy.el ends here