;;; flymake-cc.el --- Flymake support for GNU tools for C/C++     -*- lexical-binding: t; -*-

;; Copyright (C) 2018-2024 Free Software Foundation, Inc.

;; Author: João Távora <joaotavora@gmail.com>
;; Keywords: languages, c

;; This file is part of GNU Emacs.

;; GNU Emacs 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.

;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Flymake support for C/C++.

;;; Code:

(require 'cl-lib)

(defcustom flymake-cc-command 'flymake-cc-use-special-make-target
  "Command used by the `flymake-cc' backend.
A list of strings, or a symbol naming a function that produces one
such list when called with no arguments in the buffer where the
variable `flymake-mode' is active.

The command should invoke a GNU-style compiler that checks the
syntax of a (Obj)C(++) program passed to it via its standard
input and prints the result on its standard output."
  :type '(choice
          (symbol :tag "Function")
          (repeat :tag "Command(s)" string))
  :version "27.1"
  :group 'flymake-cc)

(defun flymake-cc--make-diagnostics (source)
  "Parse GNU-compatible compilation messages in current buffer.
Return a list of Flymake diagnostic objects for the source buffer
SOURCE."
  ;; TODO: if you can understand it, use `compilation-mode's regexps
  ;; or even some of its machinery here.
  ;;
  ;;    (setq-local compilation-locs
  ;;         (make-hash-table :test 'equal :weakness 'value))
  ;;    (compilation-parse-errors (point-min) (point-max)
  ;;                              'gnu 'gcc-include)
  ;;    (while (next-single-property-change 'compilation-message)
  ;;       ...)
  ;;
  ;; For now, this works minimally well.
  (cl-loop
   while
   (search-forward-regexp
    "^\\(In file included from \\)?\\([^ :]+\\):\\([0-9]+\\)\\(?::\\([0-9]+\\)\\)?:\n?\\(.*\\): \\(.*\\)$"
    nil t)
   for msg = (match-string 6)
   for locus = (match-string 2)
   for line = (string-to-number (match-string 3))
   for col = (ignore-errors (string-to-number (match-string 4)))
   for source-buffer = (and (string= locus "<stdin>") source)
   for type = (if (match-string 1)
                  :error
                (save-match-data
                  (assoc-default
                   (match-string 5)
                   '(("error" . :error)
                     ("note" . :note)
                     ("warning" . :warning))
                   #'string-match
                   :error)))
   for diag =
   (cond (source-buffer
          (pcase-let ((`(,beg . ,end)
                       (flymake-diag-region source-buffer line col)))
            (flymake-make-diagnostic source-buffer beg end type msg)))
         (t (flymake-make-diagnostic locus (cons line col) nil type msg)))
   collect diag
   ;; If "In file included from..." matched, then move to end of that
   ;; line.  This helps us collect the diagnostic at its .h locus,
   ;; too.
   when (match-end 1) do (goto-char (match-end 2))))

(defun flymake-cc-use-special-make-target ()
  "Command for checking a file via a CHK_SOURCES Make target."
  (unless (executable-find "make") (error "Make not found"))
  `("make"
    "check-syntax"
    ,(format "CHK_SOURCES=-x %s -c -"
             (cond ((derived-mode-p 'c++-mode) "c++")
                   (t "c")))))

(defvar-local flymake-cc--proc nil "Internal variable for `flymake-cc'.")

;; forward declare this to shoosh compiler (instead of requiring
;; flymake-proc)
;;
(defvar flymake-proc-allowed-file-name-masks)

;;;###autoload
(defun flymake-cc (report-fn &rest _args)
  "Flymake backend for GNU-style C compilers.
This backend uses `flymake-cc-command' (which see) to launch a
process that is passed the current buffer's contents via stdin.
REPORT-FN is Flymake's callback."
  ;; HACK: XXX: Assuming this backend function is run before it in
  ;; `flymake-diagnostic-functions', very hackingly convince the other
  ;; backend `flymake-proc-legacy-flymake', which is on by default, to
  ;; disable itself.
  ;;
  (setq-local flymake-proc-allowed-file-name-masks nil)
  (when (process-live-p flymake-cc--proc)
    (kill-process flymake-cc--proc))
  (let ((source (current-buffer)))
    (save-restriction
      (widen)
      (setq
       flymake-cc--proc
       (make-process
        :name "gcc-flymake"
        :buffer (generate-new-buffer "*gcc-flymake*")
        :command (if (symbolp flymake-cc-command)
                     (funcall flymake-cc-command)
                   flymake-cc-command)
        :noquery t :connection-type 'pipe
        :sentinel
        (lambda (p _ev)
          (unwind-protect
              (when (eq 'exit (process-status p))
                (when (with-current-buffer source (eq p flymake-cc--proc))
                  (with-current-buffer (process-buffer p)
                    (goto-char (point-min))
                    (let ((diags
                           (flymake-cc--make-diagnostics source)))
                      (if (or diags (zerop (process-exit-status p)))
                          (funcall report-fn diags)
                        ;; non-zero exit with no diags is cause
                        ;; for alarm
                        (funcall report-fn
                                 :panic :explanation
                                 (buffer-substring
                                  (point-min) (progn (goto-char (point-min))
                                                     (line-end-position)))))))))
            (unless (process-live-p p)
              ;; (display-buffer (process-buffer p)) ; uncomment to debug
              (kill-buffer (process-buffer p)))))))
      (process-send-region flymake-cc--proc (point-min) (point-max))
      (process-send-eof flymake-cc--proc))))

(provide 'flymake-cc)
;;; flymake-cc.el ends here
