|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
5 # |
|
6 |
|
7 """ |
|
8 Module implementing the syntax check for JavaScript. |
|
9 """ |
|
10 |
|
11 import multiprocessing |
|
12 import queue |
|
13 |
|
14 |
|
15 def initService(): |
|
16 """ |
|
17 Initialize the service and return the entry point. |
|
18 |
|
19 @return the entry point for the background client |
|
20 @rtype function |
|
21 """ |
|
22 return jsSyntaxCheck |
|
23 |
|
24 |
|
25 def initBatchService(): |
|
26 """ |
|
27 Initialize the batch service and return the entry point. |
|
28 |
|
29 @return the entry point for the background client |
|
30 @rtype function |
|
31 """ |
|
32 return jsSyntaxBatchCheck |
|
33 |
|
34 |
|
35 def jsSyntaxCheck(file, codestring): |
|
36 """ |
|
37 Function to check a Javascript source file for syntax errors. |
|
38 |
|
39 @param file source filename |
|
40 @type str |
|
41 @param codestring string containing the code to check |
|
42 @type str |
|
43 @return list of dictionaries with the key 'error' which contain a tuple with |
|
44 details about the syntax error. Each tuple contains the file name, line |
|
45 number, column, code string and the error message. |
|
46 @rtype list of dict |
|
47 """ |
|
48 return __jsSyntaxCheck(file, codestring) |
|
49 |
|
50 |
|
51 def jsSyntaxBatchCheck(argumentsList, send, fx, cancelled, maxProcesses=0): |
|
52 """ |
|
53 Module function to check syntax for a batch of files. |
|
54 |
|
55 @param argumentsList list of arguments tuples as given for jsSyntaxCheck |
|
56 @type list |
|
57 @param send reference to send function |
|
58 @type function |
|
59 @param fx registered service name |
|
60 @type str |
|
61 @param cancelled reference to function checking for a cancellation |
|
62 @type function |
|
63 @param maxProcesses number of processes to be used |
|
64 @type int |
|
65 """ |
|
66 if maxProcesses == 0: |
|
67 # determine based on CPU count |
|
68 try: |
|
69 NumberOfProcesses = multiprocessing.cpu_count() |
|
70 if NumberOfProcesses >= 1: |
|
71 NumberOfProcesses -= 1 |
|
72 except NotImplementedError: |
|
73 NumberOfProcesses = 1 |
|
74 else: |
|
75 NumberOfProcesses = maxProcesses |
|
76 |
|
77 # Create queues |
|
78 taskQueue = multiprocessing.Queue() |
|
79 doneQueue = multiprocessing.Queue() |
|
80 |
|
81 # Submit tasks (initially two time number of processes |
|
82 tasks = len(argumentsList) |
|
83 initialTasks = 2 * NumberOfProcesses |
|
84 for _ in range(initialTasks): |
|
85 taskQueue.put(argumentsList.pop(0)) |
|
86 |
|
87 # Start worker processes |
|
88 workers = [ |
|
89 multiprocessing.Process(target=workerTask, args=(taskQueue, doneQueue)) |
|
90 for _ in range(NumberOfProcesses) |
|
91 ] |
|
92 for worker in workers: |
|
93 worker.start() |
|
94 |
|
95 # Get and send results |
|
96 for _ in range(tasks): |
|
97 resultSent = False |
|
98 wasCancelled = False |
|
99 |
|
100 while not resultSent: |
|
101 try: |
|
102 # get result (waiting max. 3 seconds and send it to frontend |
|
103 filename, result = doneQueue.get() |
|
104 send(fx, filename, result) |
|
105 resultSent = True |
|
106 except queue.Empty: |
|
107 # ignore empty queue, just carry on |
|
108 if cancelled(): |
|
109 wasCancelled = True |
|
110 break |
|
111 |
|
112 if wasCancelled or cancelled(): |
|
113 # just exit the loop ignoring the results of queued tasks |
|
114 break |
|
115 |
|
116 if argumentsList: |
|
117 taskQueue.put(argumentsList.pop(0)) |
|
118 |
|
119 # Tell child processes to stop |
|
120 for _ in range(NumberOfProcesses): |
|
121 taskQueue.put("STOP") |
|
122 |
|
123 for worker in workers: |
|
124 worker.join() |
|
125 worker.close() |
|
126 |
|
127 taskQueue.close() |
|
128 doneQueue.close() |
|
129 |
|
130 |
|
131 def workerTask(inputQueue, outputQueue): |
|
132 """ |
|
133 Module function acting as the parallel worker for the syntax check. |
|
134 |
|
135 @param inputQueue input queue |
|
136 @type multiprocessing.Queue |
|
137 @param outputQueue output queue |
|
138 @type multiprocessing.Queue |
|
139 """ |
|
140 for filename, args in iter(inputQueue.get, "STOP"): |
|
141 source = args[0] |
|
142 result = __jsSyntaxCheck(filename, source) |
|
143 outputQueue.put((filename, result)) |
|
144 |
|
145 |
|
146 def __jsSyntaxCheck(file, codestring): |
|
147 """ |
|
148 Function to check a JavaScript source file for syntax errors. |
|
149 |
|
150 @param file source filename |
|
151 @type str |
|
152 @param codestring string containing the code to check |
|
153 @type str |
|
154 @return list of dictionaries with the key 'error' which contain a tuple with |
|
155 details about the syntax error. Each tuple contains the file name, line |
|
156 number, column, code string and the error message. |
|
157 @rtype list of dict |
|
158 """ |
|
159 if codestring: |
|
160 try: |
|
161 import esprima # noqa: I101, I102 |
|
162 except ImportError: |
|
163 error = "esprima not available. Install it via the PyPI interface." |
|
164 return [{"error": (file, 0, 0, "", error)}] |
|
165 |
|
166 try: |
|
167 esprima.parse(codestring, esnext=True, sourceType="module") |
|
168 # Parsing is just done to get syntax errors. |
|
169 except esprima.Error as exc: |
|
170 line = exc.lineNumber |
|
171 column = exc.column |
|
172 error = exc.message.split(":", 1)[-1].strip() |
|
173 |
|
174 codelines = codestring.splitlines() |
|
175 cline = min(len(codelines), line) - 1 |
|
176 code = codelines[cline] |
|
177 |
|
178 return [{"error": (file, line, column, code, error)}] |
|
179 |
|
180 return [{}] |