1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
|
;;; phpinspect-util.el --- PHP parsing and completion package -*- lexical-binding: t; -*-
;; Copyright (C) 2021-2025 Free Software Foundation, Inc
;; Author: Hugo Thunnissen <devel@hugot.nl>
;; Keywords: php, languages, tools, convenience
;; Version: 3.0.1
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;;; Code:
(defvar phpinspect-project-root-file-list
'("composer.json" "composer.lock" ".git" ".svn" ".hg")
"List of files that could indicate a project root directory.")
(defvar phpinspect--debug nil
"Enable debug logs for phpinspect by setting this variable to true")
(defun phpinspect-message (&rest args)
(let ((format-string (car args))
(args (cdr args)))
(apply #'message `(,(concat "[phpinspect] " format-string) ,@args))))
(defun phpinspect-toggle-logging ()
(interactive)
(if (setq phpinspect--debug (not phpinspect--debug))
(phpinspect-message "Enabled phpinspect logging.")
(phpinspect-message "Disabled phpinspect logging.")))
(eval-and-compile
(defvar phpinspect-log-groups nil)
(defvar phpinspect-enabled-log-groups nil)
(defvar-local phpinspect--current-log-group nil))
(define-inline phpinspect--declare-log-group (group)
(unless (and (inline-const-p group) (symbolp (inline-const-val group)))
(inline-error "Log group name should be a symbol"))
(inline-quote
(progn
(add-to-list 'phpinspect-log-groups
(cons (macroexp-file-name) ,group)))))
(defun phpinspect-log-group-enabled-p (group)
(seq-find (lambda (cons)
(eq group (cdr cons)))
phpinspect-enabled-log-groups))
(defmacro phpinspect--log (&rest args)
(let ((log-group (alist-get (macroexp-file-name)
phpinspect-log-groups nil nil #'string=)))
`(when (and phpinspect--debug
(or (not phpinspect-enabled-log-groups)
,(when log-group
`(member (quote ,log-group) phpinspect-enabled-log-groups))))
(with-current-buffer (get-buffer-create "**phpinspect-logs**")
(unless window-point-insertion-type
(set (make-local-variable 'window-point-insertion-type) t))
(goto-char (buffer-end 1))
(insert (concat "[" (format-time-string "%H:%M:%S") "]: "
,(if log-group (concat "(" (symbol-name log-group) ") ") "")
(format ,@args) "\n"))))))
(defun phpinspect-filter-logs (group-name)
(interactive (list (completing-read "Log group: "
(mapcar (lambda (g) (symbol-name (cdr g)))
phpinspect-log-groups)
nil t)))
(add-to-list 'phpinspect-enabled-log-groups (intern group-name)))
(defun phpinspect-unfilter-logs ()
(interactive)
(setq phpinspect-enabled-log-groups nil))
(defun phpinspect--find-project-root (&optional start-file)
"(Attempt to) Find the root directory of the visited PHP project.
If a found project root has a parent directory called \"vendor\",
the search continues upwards. See also
`phpinspect--locate-dominating-project-file'.
If START-FILE is provided, searching starts at the directory
level of START-FILE in stead of `default-directory`."
(let ((project-file (phpinspect--locate-dominating-project-file
(or start-file default-directory))))
(phpinspect--log "Checking for project root at %s" project-file)
(when project-file
(let* ((directory (file-name-directory project-file))
(directory-slugs (split-string (expand-file-name directory) "/")))
(if (not (member "vendor" directory-slugs))
(expand-file-name directory)
;; else. Only continue if the parent directory is not "/"
(let ((parent-without-vendor
(string-join (seq-take-while (lambda (s) (not (string= s "vendor" )))
directory-slugs)
"/")))
(when (not (or (string= parent-without-vendor "/")
(string= parent-without-vendor "")))
(phpinspect--find-project-root parent-without-vendor))))))))
(cl-defstruct (phpinspect--pattern
(:constructor phpinspect--make-pattern-generated))
"An object that can be used to match lists to a given
pattern. See `phpinspect--match-sequence'."
(matcher nil
:type lambda
:documentation "The function used to match sequences")
(code nil
:type list
:documentation "The original code list used to create this pattern"))
(defmacro phpinspect--make-pattern (&rest pattern)
`(phpinspect--make-pattern-generated
:matcher (phpinspect--match-sequence-lambda ,@pattern)
:code (list ,@(mapcar (lambda (part) (if (eq '* part) `(quote ,part) part))
pattern))))
(defun phpinspect--pattern-length (pattern)
(/ (length (phpinspect--pattern-code pattern)) 2))
(defmacro phpinspect--match-sequence-lambda (&rest pattern)
(let ((sequence-sym (gensym)))
`(lambda (,sequence-sym)
(phpinspect--match-sequence ,sequence-sym ,@pattern))))
(cl-defmethod phpinspect--pattern-match ((pattern phpinspect--pattern) sequence)
"Match SEQUENCE to PATTERN."
(funcall (phpinspect--pattern-matcher pattern) sequence))
(defun phpinspect--list-all-equal (val sequence)
(catch 'not-equal
(dolist (item sequence)
(unless (equal val item)
(throw 'not-equal nil)))
t))
(defmacro phpinspect--match-sequence (sequence &rest pattern)
"Match SEQUENCE to positional matchers defined in PATTERN.
PATTERN is a plist with the allowed keys being :m and :f. Each
key-value pair in the plist defines a match operation that is
applied to the corresponding index of SEQUENCE (so for ex.: key 0
is applied to pos. 0 of SEQUENCE, key 1 to pos. 1, and so on).
Possible match operations:
:m - This key can be used to match a list element to the literal
value supplied for it, using the `equal' comparison function. For
example, providing `(\"foobar\") as value will result in the
comparison (equal (elt SEQUENCE pos) `(\"foobar\")). There is one
exception to this rule: using the symbol * as value for the :m
key will match anything, essentially skipping comparison for the
element at this position in SEQUENCE.
:f - This key can be used to match a list element by executing
the function provided as value. The function is executed with the
list element as argument, and will be considered as matching if
it evaluates to a non-nil value."
(declare (indent 1))
(let* ((pattern-length (length pattern))
(sequence-length (/ pattern-length 2))
(sequence-pos 0)
(sequence-sym (gensym))
(match-sym (gensym))
(match-rear-sym (gensym))
(checkers (cons nil nil))
(checkers-rear checkers)
rest key value)
(while (setq key (pop pattern))
(unless (keywordp key)
(error "Invalid pattern argument, expected keyword, got: %s" key))
(unless (setq value (pop pattern))
(error "No value for key %s" key))
(cond ((eq key :m)
(unless (eq value '*)
(setq checkers-rear
(setcdr checkers-rear
(cons `(equal ,value (elt ,sequence-sym ,sequence-pos)) nil)))))
((eq key :f)
(setq checkers-rear
(setcdr
checkers-rear
(cons
(if (symbolp value)
`(,value (elt ,sequence-sym ,sequence-pos))
`(funcall ,value (elt ,sequence-sym ,sequence-pos)))
nil))))
((eq key :rest)
(setq rest value))
(t (error "Invalid keyword: %s" key)))
(setq checkers-rear
(setcdr checkers-rear
(cons `(setq ,match-rear-sym
(setcdr ,match-rear-sym
(cons (elt ,sequence-sym ,sequence-pos) nil)))
nil)))
(setq sequence-pos (+ sequence-pos 1)))
(setq checkers (cdr checkers))
`(let* ((,sequence-sym ,sequence)
(,match-sym (cons nil nil))
(,match-rear-sym ,match-sym))
,(if rest
`(and ,@checkers
(cdr ,match-sym)
,(if (eq rest '*)
't
`(phpinspect--list-all-equal ,rest (nthcdr ,sequence-length ,sequence-sym))))
`(and (= ,sequence-length (length ,sequence))
,@checkers
(cdr ,match-sym))))))
(defun phpinspect--pattern-concat (pattern1 pattern2)
(let* ((pattern1-sequence-length (/ (length (phpinspect--pattern-code pattern1)) 2)))
(phpinspect--make-pattern-generated
:matcher (lambda (sequence)
(unless (< (length sequence) pattern1-sequence-length)
(and (phpinspect--pattern-match
pattern1
(butlast sequence (- (length sequence) pattern1-sequence-length)))
(phpinspect--pattern-match
pattern2
(last sequence (- (length sequence) pattern1-sequence-length))))))
:code (append (phpinspect--pattern-code pattern1)
(phpinspect--pattern-code pattern2)))))
(defun phpinspect--locate-dominating-project-file (start-file)
"Locate the first dominating file in `phpinspect-project-root-file-list`.
Starts looking at START-FILE and then recurses up the directory
hierarchy as long as no matching files are found. See also
`locate-dominating-file'."
(let ((dominating-file))
(seq-find (lambda (file)
(setq dominating-file (locate-dominating-file start-file file)))
phpinspect-project-root-file-list)
dominating-file))
(defun phpinspect--determine-completion-point ()
"Find first point backwards that could contain any kind of
context for completion."
(save-excursion
(re-search-backward "[^[:blank:]\n]" nil t)
(forward-char)
(point)))
(defmacro phpinspect-json-preset (&rest body)
"Default options to wrap around `json-read' and similar BODY."
`(let ((json-object-type 'hash-table)
(json-array-type 'list)
(json-key-type 'string))
,@body))
(defun phpinspect--input-pending-p (&optional check-timers)
(and (input-pending-p check-timers)
(not noninteractive)))
(defun phpinspect-thread-pause (pause-time mx continue)
"Pause current thread using MX and CONTINUE for PAUSE-TIME idle seconds.
PAUSE-TIME must be the idle time that the thread should pause for.
MX must be a mutex
CONTINUE must be a condition-variable"
(phpinspect--log "Thread '%s' is paused for %d seconds" (thread-name (current-thread)) pause-time)
(run-with-idle-timer
pause-time
nil
(lambda () (with-mutex mx (condition-notify continue))))
(ignore-errors (with-mutex mx (condition-wait continue)))
(phpinspect--log "Thread '%s' continuing execution" (thread-name (current-thread))))
(provide 'phpinspect-util)
;;; phpinspect-util.el ends here
|