;;; phpinspect.el --- PHP parsing and completion package -*- lexical-binding: t; -*- ;; Copyright (C) 2021-2023 Free Software Foundation, Inc ;; Author: Hugo Thunnissen ;; Keywords: php, languages, tools, convenience ;; Version: 0 ;; 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: ;;; Code: (require 'phpinspect-project) (require 'phpinspect-autoload) (require 'phpinspect-worker) (defcustom phpinspect-load-stubs t "If and when phpinspect should load code stubs." :type '(choice (const :tag "Load stubs on first mode init." t) (const :tag "Never load stubs." nil)) :group 'phpinspect) (defvar phpinspect-buffers (make-hash-table :test #'eq) "All buffers for which `phpinspect-mode' is currently active. Hash table with buffer (native emacs buffer object, `bufferp') as key, and a reset-function as value. The reset-function is called without arguments when the cache is purged (see `phpinspect-purge-cache'.") (defun phpinspect-register-current-buffer (reset-func) (puthash (current-buffer) reset-func phpinspect-buffers)) (defun phpinspect-unregister-current-buffer () (remhash (current-buffer) phpinspect-buffers)) (defvar phpinspect-stub-cache nil "An instance of `phpinspect--cache' containing an index of PHP functions and classes which phpinspect preloads. This index is not supposed to be mutated after initial creation.") (defmacro phpinspect--cache-edit (cache &rest body) (declare (indent 1)) `(unless (phpinspect--cache-read-only-p ,cache) ,@body)) (defvar phpinspect-cache nil "An object used to store and access metadata of PHP projects.") (cl-defstruct (phpinspect--cache (:constructor phpinspect--make-cache)) (read-only-p nil :type boolean :documentation "Whether this cache instance is read-only, meaning that it's data should never be changed. When the value of this slot is non-nil: - Actions that would normally mutate it's data should become no-ops. - All projects that are retrieved from it should be marked as read-only as well.") (extra-class-retriever nil :type lambda :documentation "A function that should accept a `phpinspect--type' and return matching `phpinspect--class' instances or nil. Used to discover classes that are defined outside of code that this cache knows about.") (extra-function-retriever nil :type lambda :documentation "A function that should accept a `phpinspect-name' (see `phpinspect-intern-name') and return matching `phpinspect--function' instances or nil. Used to discover functions that are defined outside of code that this cache knows about.") (projects (make-hash-table :test 'equal :size 10) :type hash-table :documentation "A `hash-table` with the root directories of projects as keys and project caches as values.")) (defun phpinspect--get-stub-class (fqn) (when phpinspect-stub-cache (phpinspect--log "Getting stub class") (catch 'return (maphash (lambda (_name project) (when-let ((class (phpinspect-project-get-class project fqn))) (throw 'return class))) (phpinspect--cache-projects phpinspect-stub-cache))))) (defun phpinspect--get-stub-function (name) (when phpinspect-stub-cache (if name (catch 'return (phpinspect--log "Getting stub function by name %s" name) (maphash (lambda (_name project) (when-let ((class (phpinspect-project-get-function project name))) (throw 'return class))) (phpinspect--cache-projects phpinspect-stub-cache))) (let* ((funcs (cons nil nil)) (funcs-rear funcs)) (phpinspect--log "Retrieving all stub functions for nil name") (maphash (lambda (_name project) (setq funcs-rear (last (nconc funcs-rear (phpinspect-project-get-functions project))))) (phpinspect--cache-projects phpinspect-stub-cache)) (cdr funcs))))) (defun phpinspect--get-or-create-global-cache () "Get `phpinspect-cache'. If its value is nil, it is created and then returned." (or phpinspect-cache (setq phpinspect-cache (phpinspect--make-cache :extra-class-retriever #'phpinspect--get-stub-class :extra-function-retriever #'phpinspect--get-stub-function)))) (defun phpinspect-purge-cache () "Assign a fresh, empty cache object to `phpinspect-cache'. This effectively purges any cached code information from all currently opened projects." (interactive) (when phpinspect-cache ;; Allow currently known cached projects to cleanup after themselves (maphash (lambda (_ project) (phpinspect-project-purge project)) (phpinspect--cache-projects phpinspect-cache))) (maphash (lambda (buffer reset-hook) (with-current-buffer buffer (funcall reset-hook))) phpinspect-buffers) ;; Assign a fresh cache object (setq phpinspect-cache (phpinspect--get-or-create-global-cache)) (setq phpinspect-names (phpinspect-make-name-hash)) (phpinspect-define-standard-types)) (cl-defmethod phpinspect--cache-get-project ((cache phpinspect--cache) (project-root string)) (let ((project (gethash project-root (phpinspect--cache-projects cache)))) (when (and project (phpinspect--cache-read-only-p cache) (not (phpinspect-project-read-only-p project))) (setf (phpinspect-project-read-only-p project) t)) project)) (defun phpinspect-get-or-create-cached-project-class (project-root class-fqn) (when project-root (let ((project (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) project-root))) (phpinspect-project-get-class-extra-or-create project class-fqn)))) (cl-defmethod phpinspect--cache-get-project-create ((cache phpinspect--cache) (project-root string)) "Get a project that is located in PROJECT-ROOT from CACHE. If no such project exists in the cache yet, it is created and then returned." (let ((project (phpinspect--cache-get-project cache project-root))) (unless project (phpinspect--cache-edit cache (setq project (puthash project-root (phpinspect--make-project :fs (phpinspect-make-fs) :root project-root :extra-class-retriever (phpinspect--cache-extra-class-retriever cache) :extra-function-retriever (phpinspect--cache-extra-function-retriever cache) :worker (phpinspect-make-dynamic-worker)) (phpinspect--cache-projects cache))) (let ((autoloader (phpinspect-make-autoloader :fs (phpinspect-project-fs project) :file-indexer (phpinspect-project-make-file-indexer project) :project-root-resolver (phpinspect-project-make-root-resolver project)))) (setf (phpinspect-project-autoload project) autoloader) (phpinspect-autoloader-refresh autoloader) (phpinspect-project-enqueue-include-dirs project)))) project)) (defun phpinspect-project-enqueue-include-dirs (project) (interactive (list (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) (phpinspect-current-project-root)))) (phpinspect-project-edit project (let ((dirs (alist-get 'include-dirs (alist-get (phpinspect-project-root project) phpinspect-projects nil nil #'string=)))) (dolist (dir dirs) (phpinspect-message "enqueueing dir %s" dir) (phpinspect-worker-enqueue (phpinspect-project-worker project) (phpinspect-make-index-dir-task :dir dir :project project)))))) (defun phpinspect-project-add-include-dir (dir) "Configure DIR as an include dir for the current project." (interactive (list (read-directory-name "Include Directory: "))) (custom-set-variables '(phpinspect-projects)) (let ((existing (alist-get (phpinspect-current-project-root) phpinspect-projects nil #'string=))) (if existing (push dir (alist-get 'include-dirs existing)) (push `(,(phpinspect-current-project-root) . ((include-dirs . (,dir)))) phpinspect-projects))) (customize-save-variable 'phpinspect-projects phpinspect-projects) (phpinspect-project-enqueue-include-dirs (phpinspect--cache-get-project-create (phpinspect--get-or-create-global-cache) (phpinspect-current-project-root)))) (defconst phpinspect-stub-directory (expand-file-name "stubs" (file-name-directory (macroexp-file-name))) "Directory where PHP stub files are located.") (defconst phpinspect-data-directory (expand-file-name "data" (file-name-directory (macroexp-file-name))) "Directory for data distributed with phpinspect.") (defconst phpinspect-stub-cache-file (expand-file-name "builtin-stubs.eld" phpinspect-data-directory) "") (defconst phpinspect-builtin-index-file (expand-file-name (concat "builtin-stubs-index.eld" (if (zlib-available-p) ".gz" "")) phpinspect-data-directory) "") (defun phpinspect-build-stub-cache () (let* ((cache (phpinspect--make-cache)) (builtin-project (phpinspect--cache-get-project-create cache "builtins")) (phpinspect-worker 'nil-worker)) (phpinspect-project-add-index builtin-project (phpinspect-build-stub-index)))) (defun phpinspect-build-stub-index () (phpinspect--index-tokens (phpinspect-parse-file (expand-file-name "builtins.php" phpinspect-stub-directory)))) (defun phpinspect-dump-stub-index () (interactive) (let* ((phpinspect-names (phpinspect-make-name-hash)) (index (phpinspect-build-stub-index))) (with-temp-buffer (let ((print-length nil) (print-level nil) (print-circle t)) (prin1 (list (cons 'names phpinspect-names) (cons 'index index)) (current-buffer)) (write-file phpinspect-builtin-index-file))))) (defun phpinspect-load-stub-index () (interactive) (unless (file-exists-p phpinspect-builtin-index-file) (phpinspect-message "No stub index dump found, dumping stub index ...") (phpinspect-dump-stub-index)) (let* ((data (with-temp-buffer (insert-file-contents phpinspect-builtin-index-file) (goto-char (point-min)) (read (current-buffer)))) (project (phpinspect--make-project :worker 'nil-worker))) (phpinspect-purge-cache) (setq phpinspect-names (alist-get 'names data)) (phpinspect-define-standard-types) (setq phpinspect-stub-cache (phpinspect--make-cache)) (phpinspect-project-add-index project (alist-get 'index data)) (puthash "builtins" project (phpinspect--cache-projects phpinspect-stub-cache)) (setf (phpinspect--cache-read-only-p phpinspect-stub-cache) t))) ;;; phpinspect.el ends here (provide 'phpinspect-cache)