• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oracle / opengrok / #3691

09 Nov 2023 04:15PM UTC coverage: 74.721% (+8.6%) from 66.08%
#3691

push

web-flow
avoid annotations for binary files (#4476)

fixes #4473

6 of 13 new or added lines in 4 files covered. (46.15%)

258 existing lines in 28 files now uncovered.

43797 of 58614 relevant lines covered (74.72%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.56
/opengrok-indexer/src/main/java/org/opengrok/indexer/analysis/Ctags.java
1
/*
2
 * CDDL HEADER START
3
 *
4
 * The contents of this file are subject to the terms of the
5
 * Common Development and Distribution License (the "License").
6
 * You may not use this file except in compliance with the License.
7
 *
8
 * See LICENSE.txt included in this distribution for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing Covered Code, include this CDDL HEADER in each
12
 * file and include the License file at LICENSE.txt.
13
 * If applicable, add the following below this CDDL HEADER, with the
14
 * fields enclosed by brackets "[]" replaced with your own identifying
15
 * information: Portions Copyright [yyyy] [name of copyright owner]
16
 *
17
 * CDDL HEADER END
18
 */
19

20
/*
21
 * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved.
22
 * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
23
 */
24
package org.opengrok.indexer.analysis;
25

26
import java.io.BufferedReader;
27
import java.io.File;
28
import java.io.IOException;
29
import java.io.InputStream;
30
import java.io.InputStreamReader;
31
import java.io.OutputStream;
32
import java.io.OutputStreamWriter;
33
import java.io.StringReader;
34
import java.nio.charset.StandardCharsets;
35
import java.time.Duration;
36
import java.util.ArrayList;
37
import java.util.Collections;
38
import java.util.HashSet;
39
import java.util.List;
40
import java.util.Set;
41
import java.util.concurrent.ExecutionException;
42
import java.util.concurrent.ExecutorService;
43
import java.util.concurrent.Future;
44
import java.util.concurrent.TimeUnit;
45
import java.util.concurrent.TimeoutException;
46
import java.util.logging.Level;
47
import java.util.logging.Logger;
48

49
import org.apache.commons.lang3.SystemUtils;
50
import org.jetbrains.annotations.Nullable;
51
import org.opengrok.indexer.configuration.OpenGrokThreadFactory;
52
import org.opengrok.indexer.configuration.RuntimeEnvironment;
53
import org.opengrok.indexer.index.IndexerParallelizer;
54
import org.opengrok.indexer.logger.LoggerFactory;
55
import org.opengrok.indexer.util.CtagsUtil;
56
import org.opengrok.indexer.util.Executor;
57
import org.opengrok.indexer.util.IOUtils;
58
import org.opengrok.indexer.util.SourceSplitter;
59

60
/**
61
 * Provides Ctags by having a running subprocess of ctags.
62
 *
63
 * @author Chandan
64
 */
65
public class Ctags implements Resettable {
66

67
    private static final Logger LOGGER = LoggerFactory.getLogger(Ctags.class);
1✔
68

69
    private final RuntimeEnvironment env;
70
    private volatile boolean closing;
71
    private LangMap langMap;
72
    private List<String> command;
73
    private Process ctagsProcess;
74
    private OutputStreamWriter ctagsIn;
75
    private BufferedReader ctagsOut;
76
    private static final String CTAGS_FILTER_TERMINATOR = "__ctags_done_with_file__";
77
    private String cTagsExtraOptionsFile = null;
1✔
78
    private int tabSize;
79
    private Duration timeout = Duration.ofSeconds(10);
1✔
80

81
    private final Set<String> ctagsLanguages = new HashSet<>();
1✔
82

83
    private boolean junitTesting = false;
1✔
84

85
    /**
86
     * Initializes an instance with the current
87
     * {@link AnalyzerGuru#getLangMap()}.
88
     */
89
    public Ctags() {
1✔
90
        env = RuntimeEnvironment.getInstance();
1✔
91
        langMap = AnalyzerGuru.getLangMap();
1✔
92
        cTagsExtraOptionsFile = env.getCTagsExtraOptionsFile();
1✔
93
    }
1✔
94

95
    /**
96
     * Gets a value indicating if a subprocess of ctags was started, and it is not alive.
97
     * @return {@code true} if the instance should be considered closed and no longer usable.
98
     */
99
    public boolean isClosed() {
100
        return ctagsProcess != null && !ctagsProcess.isAlive();
1✔
101
    }
102

103
    public void setLangMap(LangMap langMap) {
104
        this.langMap = langMap;
×
105
    }
×
106

107
    public int getTabSize() {
108
        return tabSize;
×
109
    }
110

111
    public void setTabSize(int tabSize) {
112
        this.tabSize = tabSize;
1✔
113
    }
1✔
114

115
    public void setCTagsExtraOptionsFile(String ctagsExtraOptionsFile) {
116
        this.cTagsExtraOptionsFile = ctagsExtraOptionsFile;
1✔
117
    }
1✔
118

119
    public void setTimeout(long timeout) {
120
        this.timeout = Duration.ofSeconds(timeout);
1✔
121
    }
1✔
122

123
    public long getTimeout() {
124
        return this.timeout.getSeconds();
1✔
125
    }
126

127
    /**
128
     * Resets the instance for use for another file but without closing any
129
     * running ctags instance.
130
     */
131
    @Override
132
    public void reset() {
133
        setTabSize(0);
1✔
134
    }
1✔
135

136
    /**
137
     * {@link #reset()}, and close any running ctags instance.
138
     */
139
    public void close() {
140
        reset();
1✔
141
        IOUtils.close(ctagsIn);
1✔
142
        if (ctagsProcess != null) {
1✔
143
            closing = true;
1✔
144
            LOGGER.log(Level.FINE, "Destroying ctags command");
1✔
145
            ctagsProcess.destroyForcibly();
1✔
146
        }
147
    }
1✔
148

149
    /**
150
     * Gets the command-line arguments used to run ctags.
151
     * @return a defined (immutable) list
152
     */
153
    public List<String> getArgv() {
154
        initialize();
×
155
        return Collections.unmodifiableList(command);
×
156
    }
157

158
    private void initialize() {
159
        command = new ArrayList<>();
1✔
160
        String ctagsCommand = env.getCtags();
1✔
161
        command.add(ctagsCommand);
1✔
162

163
        // Normally, the indexer or the webapp will call validateUniversalCtags()
164
        // that would set the set of ctags languages returned by env.getCtagsLanguages(),
165
        // however for tests this might not be always the case so do it here.
166
        if (env.getCtagsLanguages().isEmpty()) {
1✔
167
            ctagsLanguages.addAll(CtagsUtil.getLanguages(ctagsCommand));
1✔
168
        } else {
169
            ctagsLanguages.addAll(env.getCtagsLanguages());
1✔
170
        }
171

172
        command.add("--kinds-c=+l");
1✔
173

174
        // Workaround for bug #14924: Don't get local variables in Java
175
        // code since that creates many false positives.
176
        // CtagsTest : bug14924 "too many methods" guards for this
177
        // universal ctags are however safe, so enabling for them
178
        command.add("--kinds-java=+l");
1✔
179

180
        command.add("--kinds-sql=+l");
1✔
181
        command.add("--kinds-Fortran=+L");
1✔
182
        command.add("--kinds-C++=+l");
1✔
183
        command.add("--extras=+F"); // Replacement for `--file-scope=yes` since 2017
1✔
184
        command.add("-u"); // Equivalent to `--sort=no` (i.e. "unsorted")
1✔
185
        command.add("--filter=yes");
1✔
186
        command.add("--filter-terminator=" + CTAGS_FILTER_TERMINATOR + "\n");
1✔
187
        command.add("--fields=-af+iKnS");
1✔
188
        command.add("--excmd=pattern");
1✔
189
        command.add("--pattern-length-limit=180"); // Increase from default 96
1✔
190

191
        //Ideally all below should be in ctags, or in outside config file,
192
        //we might run out of command line SOON
193
        //Also note, that below ctags definitions HAVE to be in POSIX
194
        //otherwise the regexp will not work on some platforms
195
        //on Solaris regexp.h used is different than on linux (gnu regexp)
196
        //http://en.wikipedia.org/wiki/Regular_expression#POSIX_basic_and_extended
197
        addScalaSupport(command);
1✔
198
        addHaskellSupport(command);
1✔
199
        //temporarily use our defs until ctags will fix https://github.com/universal-ctags/ctags/issues/988
200
        addClojureSupport(command);
1✔
201
        addKotlinSupport(command);
1✔
202
        addSwiftSupport(command);
1✔
203
        addRustSupport(command);
1✔
204
        addPascalSupport(command);
1✔
205
        addPowerShellSupport(command);
1✔
206
        //PLEASE add new languages ONLY with POSIX syntax (see above wiki link)
207

208
        if (langMap == null) {
1✔
209
            LOGGER.warning("langMap property is null");
×
210
        } else {
211
            command.addAll(langMap.getCtagsArgs());
1✔
212
        }
213

214
        /* Add extra command line options for ctags. */
215
        if (cTagsExtraOptionsFile != null) {
1✔
216
            LOGGER.log(Level.FINER, "Adding extra options to ctags");
1✔
217
            command.add("--options=" + cTagsExtraOptionsFile);
1✔
218
        }
219
    }
1✔
220

221
    private void run() throws IOException {
222
        String commandStr = Executor.escapeForShell(command, false, SystemUtils.IS_OS_WINDOWS);
1✔
223
        LOGGER.log(Level.FINE, "Executing ctags command [{0}]", commandStr);
1✔
224

225
        ProcessBuilder processBuilder = new ProcessBuilder(command);
1✔
226

227
        ctagsProcess = processBuilder.start();
1✔
228
        ctagsIn = new OutputStreamWriter(ctagsProcess.getOutputStream(), StandardCharsets.UTF_8);
1✔
229
        ctagsOut = new BufferedReader(new InputStreamReader(ctagsProcess.getInputStream(),
1✔
230
            StandardCharsets.UTF_8));
231

232
        Thread errThread = new OpenGrokThreadFactory("ctags-err").newThread(() -> {
1✔
233
            try (BufferedReader error = new BufferedReader(new InputStreamReader(ctagsProcess.getErrorStream(),
1✔
234
                    StandardCharsets.UTF_8))) {
235
                String s;
236
                while ((s = error.readLine()) != null) {
1✔
237
                    if (s.length() > 0) {
1✔
238
                        LOGGER.log(Level.WARNING, "Error from ctags: {0}", s);
1✔
239
                    }
240
                    if (closing) {
1✔
UNCOV
241
                        break;
×
242
                    }
243
                }
244
            } catch (IOException exp) {
×
245
                LOGGER.log(Level.WARNING, "Got an exception reading ctags error stream: ", exp);
×
246
            }
1✔
247
        });
1✔
248
        errThread.setDaemon(true);
1✔
249
        errThread.start();
1✔
250
    }
1✔
251

252
    private void addRustSupport(List<String> command) {
253
        if (!ctagsLanguages.contains("Rust")) { // Built-in would be capitalized.
1✔
254
            command.add("--langdef=rust"); // Lower-case if user-defined.
×
255
        }
256

257
        // The following are not supported yet in Universal Ctags 882b6c7
258
        command.add("--kinddef-rust=I,impl,Trait\\ implementation");
1✔
259
        command.add("--kinddef-rust=r,trait,Traits");
1✔
260
        command.add("--kinddef-rust=V,localVariable,Local\\ variables");
1✔
261
        command.add("--regex-rust=/^[[:space:]]*(pub[[:space:]]+)?(static|const)[[:space:]]+(mut[[:space:]]+)?" +
1✔
262
                "([[:alnum:]_]+)/\\4/C/");
263
        command.add("--regex-rust=/^[[:space:]]*(pub[[:space:]]+)?(unsafe[[:space:]]+)?impl([[:space:]\n]*<[^>]*>)?" +
1✔
264
                "[[:space:]]+(([[:alnum:]_:]+)[[:space:]]*(<[^>]*>)?[[:space:]]+(for)[[:space:]]+)?" +
265
                "([[:alnum:]_]+)/\\5 \\7 \\8/I/");
266
        command.add("--regex-rust=/^[[:space:]]*(pub[[:space:]]+)?(unsafe[[:space:]]+)?trait[[:space:]]+([[:alnum:]_]+)/\\3/r/");
1✔
267
        command.add("--regex-rust=/^[[:space:]]*let([[:space:]]+mut)?[[:space:]]+([[:alnum:]_]+)/\\2/V/");
1✔
268
    }
1✔
269

270
    private void addPowerShellSupport(List<String> command) {
271
        if (!ctagsLanguages.contains("PowerShell")) { // Built-in would be capitalized.
1✔
272
            command.add("--langdef=powershell"); // Lower-case if user-defined.
×
273
        }
274

275
        command.add("--regex-powershell=/\\$(\\{[^}]+\\})/\\1/v/");
1✔
276
        command.add("--regex-powershell=/\\$([[:alnum:]_]+([:.][[:alnum:]_]+)*)/\\1/v/");
1✔
277
        command.add("--regex-powershell=/^[[:space:]]*(:[^[:space:]]+)/\\1/l,label/");
1✔
278

279
        command.add("--_fielddef-powershell=signature,signatures");
1✔
280
        command.add("--fields-powershell=+{signature}");
1✔
281
        // escaped variable markers
282
        command.add("--regex-powershell=/`\\$([[:alnum:]_]+([:.][[:alnum:]_]+)*)/\\1//{exclusive}");
1✔
283
        command.add("--regex-powershell=/`\\$(\\{[^}]+\\})/\\1//{exclusive}");
1✔
284
        command.add("--regex-powershell=/#.*\\$([[:alnum:]_]+([:.][[:alnum:]_]+)*)/\\1//{exclusive}");
1✔
285
        command.add("--regex-powershell=/#.*\\$(\\{[^}]+\\})/\\1//{exclusive}");
1✔
286
        command.add("--regex-powershell=/^[[:space:]]*(function|filter)[[:space:]]+([^({[:space:]]+)[[:space:]]*" +
1✔
287
                "(\\(([^)]+)\\))?/\\2/f/{icase}{exclusive}{_field=signature:(\\4)}");
288
    }
1✔
289

290
    private void addPascalSupport(List<String> command) {
291
        if (!ctagsLanguages.contains("Pascal")) { // Built-in would be capitalized.
1✔
292
            command.add("--langdef=pascal"); // Lower-case if user-defined.
×
293
        }
294

295
        command.add("--kinddef-pascal=t,type,Types");
1✔
296
        command.add("--kinddef-pascal=c,class,Classes");
1✔
297
        command.add("--kinddef-pascal=i,interface,Interfaces");
1✔
298
        command.add("--kinddef-pascal=n,constructor,Constructors");
1✔
299
        command.add("--kinddef-pascal=d,destructor,Destructors");
1✔
300
        command.add("--kinddef-pascal=o,property,Properties");
1✔
301
        command.add("--kinddef-pascal=s,section,Sections");
1✔
302
        command.add("--kinddef-pascal=u,unit,Units");
1✔
303
        command.add("--regex-pascal=/([[:alnum:]_]+)[[:space:]]*=[[:space:]]*\\([[:space:]]*[[:alnum:]_][[:space:]]*\\)/\\1/t/");
1✔
304
        command.add("--regex-pascal=/([[:alnum:]_]+)[[:space:]]*=[[:space:]]*class[[:space:]]*[^;]*$/\\1/c/");
1✔
305
        command.add("--regex-pascal=/([[:alnum:]_]+)[[:space:]]*=[[:space:]]*interface[[:space:]]*[^;]*$/\\1/i/");
1✔
306
        command.add("--regex-pascal=/^constructor[[:space:]]+(T[a-zA-Z0-9_]+(<[a-zA-Z0-9_, ]+>)?\\.)([a-zA-Z0-9_<>, ]+)(.*)+/\\1\\3/n/");
1✔
307
        command.add("--regex-pascal=/^destructor[[:space:]]+(T[a-zA-Z0-9_]+(<[a-zA-Z0-9_, ]+>)?\\.)([a-zA-Z0-9_<>, ]+)(.*)+/\\1\\3/d/");
1✔
308
        command.add("--regex-pascal=/^(procedure)[[:space:]]+T[a-zA-Z0-9_<>, ]+\\.([a-zA-Z0-9_<>, ]+)(.*)/\\2/p/");
1✔
309
        command.add("--regex-pascal=/^(function)[[:space:]]+T[a-zA-Z0-9_<>, ]+\\.([a-zA-Z0-9_<>, ]+)(.*)/\\2/f/");
1✔
310
        command.add("--regex-pascal=/^[[:space:]]*property[[:space:]]+([a-zA-Z0-9_<>, ]+)[[:space:]]*\\:(.*)/\\1/o/");
1✔
311
        command.add("--regex-pascal=/^(uses|interface|implementation)$/\\1/s/");
1✔
312
        command.add("--regex-pascal=/^unit[[:space:]]+([a-zA-Z0-9_<>, ]+)[;(]/\\1/u/");
1✔
313
    }
1✔
314

315
    private void addSwiftSupport(List<String> command) {
316
        if (!ctagsLanguages.contains("Swift")) { // Built-in would be capitalized.
1✔
317
            command.add("--langdef=swift"); // Lower-case if user-defined.
1✔
318
        }
319
        command.add("--kinddef-swift=n,enum,Enums");
1✔
320
        command.add("--kinddef-swift=t,typealias,Type\\ aliases");
1✔
321
        command.add("--kinddef-swift=p,protocol,Protocols");
1✔
322
        command.add("--kinddef-swift=s,struct,Structs");
1✔
323
        command.add("--kinddef-swift=c,class,Classes");
1✔
324
        command.add("--kinddef-swift=f,function,Functions");
1✔
325
        command.add("--kinddef-swift=v,variable,Variables");
1✔
326
        command.add("--kinddef-swift=e,extension,Extensions");
1✔
327

328
        command.add("--regex-swift=/enum[[:space:]]+([^\\{\\}]+).*$/\\1/n/");
1✔
329
        command.add("--regex-swift=/typealias[[:space:]]+([^:=]+).*$/\\1/t/");
1✔
330
        command.add("--regex-swift=/protocol[[:space:]]+([^:\\{]+).*$/\\1/p/");
1✔
331
        command.add("--regex-swift=/struct[[:space:]]+([^:\\{]+).*$/\\1/s/");
1✔
332
        command.add("--regex-swift=/class[[:space:]]+([^:\\{]+).*$/\\1/c/");
1✔
333
        command.add("--regex-swift=/func[[:space:]]+([^\\(\\)]+)\\([^\\(\\)]*\\)/\\1/f/");
1✔
334
        command.add("--regex-swift=/(var|let)[[:space:]]+([^:=]+).*$/\\2/v/");
1✔
335
        command.add("--regex-swift=/^[[:space:]]*extension[[:space:]]+([^:\\{]+).*$/\\1/e/");
1✔
336
    }
1✔
337

338
    private void addKotlinSupport(List<String> command) {
339
        if (!ctagsLanguages.contains("Kotlin")) { // Built-in would be capitalized.
1✔
340
            command.add("--langdef=kotlin"); // Lower-case if user-defined.
×
341
        }
342
        command.add("--kinddef-kotlin=d,dataClass,Data\\ classes");
1✔
343
        command.add("--kinddef-kotlin=I,import,Imports");
1✔
344

345
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
346
                "(private[^ ]*|protected)?[[:space:]]*class[[:space:]]+([[:alnum:]_:]+)/\\4/c/");
347
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
348
                "(private[^ ]*|protected)?[[:space:]]*object[[:space:]]+([[:alnum:]_:]+)/\\4/o/");
349
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
350
                "(private[^ ]*|protected)?[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
351
                "data class[[:space:]]+([[:alnum:]_:]+)/\\6/d/");
352
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
353
                "(private[^ ]*|protected)?[[:space:]]*interface[[:space:]]+([[:alnum:]_:]+)/\\4/i/");
354
        command.add("--regex-kotlin=/^[[:space:]]*type[[:space:]]+([[:alnum:]_:]+)/\\1/T/");
1✔
355
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy|private[^ ]*" +
1✔
356
                "(\\[[a-z]*\\])*|protected)[[:space:]]*)*fun[[:space:]]+([[:alnum:]_:]+)/\\4/m/");
357
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy|private[^ ]*" +
1✔
358
                "|protected)[[:space:]]*)*val[[:space:]]+([[:alnum:]_:]+)/\\3/C/");
359
        command.add("--regex-kotlin=/^[[:space:]]*((abstract|final|sealed|implicit|lazy|private[^ ]*" +
1✔
360
                "|protected)[[:space:]]*)*var[[:space:]]+([[:alnum:]_:]+)/\\3/v/");
361
        command.add("--regex-kotlin=/^[[:space:]]*package[[:space:]]+([[:alnum:]_.:]+)/\\1/p/");
1✔
362
        command.add("--regex-kotlin=/^[[:space:]]*import[[:space:]]+([[:alnum:]_.:]+)/\\1/I/");
1✔
363
    }
1✔
364

365
    /**
366
     * Override Clojure support with patterns from https://gist.github.com/kul/8704283.
367
     */
368
    private void addClojureSupport(List<String> command) {
369
        if (!ctagsLanguages.contains("Clojure")) { // Built-in would be capitalized.
1✔
370
            command.add("--langdef=clojure"); // Lower-case if user-defined.
×
371
        }
372
        command.add("--kinddef-clojure=d,definition,Definitions");
1✔
373
        command.add("--kinddef-clojure=p,privateFunction,Private\\ functions");
1✔
374
        command.add("--kinddef-clojure=m,macro,Macros");
1✔
375
        command.add("--kinddef-clojure=i,inline,Inlines");
1✔
376
        command.add("--kinddef-clojure=a,multimethodDefinition,Multimethod\\ definitions");
1✔
377
        command.add("--kinddef-clojure=b,multimethodInstance,Multimethod\\ instances");
1✔
378
        command.add("--kinddef-clojure=c,definitionOnce,Definition\\ once");
1✔
379
        command.add("--kinddef-clojure=s,struct,Structs");
1✔
380
        command.add("--kinddef-clojure=v,intern,Interns");
1✔
381

382
        command.add("--regex-clojure=/\\([[:space:]]*create-ns[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/n/");
1✔
383
        command.add("--regex-clojure=/\\([[:space:]]*def[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/d/");
1✔
384
        command.add("--regex-clojure=/\\([[:space:]]*defn-[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/p/");
1✔
385
        command.add("--regex-clojure=/\\([[:space:]]*defmacro[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/m/");
1✔
386
        command.add("--regex-clojure=/\\([[:space:]]*definline[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/i/");
1✔
387
        command.add("--regex-clojure=/\\([[:space:]]*defmulti[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/a/");
1✔
388
        command.add("--regex-clojure=/\\([[:space:]]*defmethod[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/b/");
1✔
389
        command.add("--regex-clojure=/\\([[:space:]]*defonce[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/c/");
1✔
390
        command.add("--regex-clojure=/\\([[:space:]]*defstruct[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/s/");
1✔
391
        command.add("--regex-clojure=/\\([[:space:]]*intern[[:space:]]+([-[:alnum:]*+!_:\\/.?]+)/\\1/v/");
1✔
392
    }
1✔
393

394
    private void addHaskellSupport(List<String> command) {
395
        if (!ctagsLanguages.contains("Haskell")) { // Built-in would be capitalized.
1✔
396
            command.add("--langdef=haskell"); // below added with #912. Lowercase if user-defined.
×
397
        }
398

399
        command.add("--regex-haskell=/^[[:space:]]*class[[:space:]]+([a-zA-Z0-9_]+)/\\1/c/");
1✔
400
        command.add("--regex-haskell=/^[[:space:]]*data[[:space:]]+([a-zA-Z0-9_]+)/\\1/t/");
1✔
401
        command.add("--regex-haskell=/^[[:space:]]*newtype[[:space:]]+([a-zA-Z0-9_]+)/\\1/t/");
1✔
402
        command.add("--regex-haskell=/^[[:space:]]*type[[:space:]]+([a-zA-Z0-9_]+)/\\1/t/");
1✔
403
        command.add("--regex-haskell=/^([a-zA-Z0-9_]+).*[[:space:]]+={1}[[:space:]]+/\\1/f/");
1✔
404
        command.add("--regex-haskell=/[[:space:]]+([a-zA-Z0-9_]+).*[[:space:]]+={1}[[:space:]]+/\\1/f/");
1✔
405
        command.add("--regex-haskell=/^(let|where)[[:space:]]+([a-zA-Z0-9_]+).*[[:space:]]+={1}[[:space:]]+/\\2/f/");
1✔
406
        command.add("--regex-haskell=/[[:space:]]+(let|where)[[:space:]]+([a-zA-Z0-9_]+).*[[:space:]]+={1}[[:space:]]+/\\2/f/");
1✔
407
    }
1✔
408

409
    private void addScalaSupport(List<String> command) {
410
        if (!ctagsLanguages.contains("Scala")) { // Built-in would be capitalized.
1✔
411
            command.add("--langdef=scala"); // below is bug 61 to get full scala support. Lower-case
1✔
412
        }
413
        command.add("--kinddef-scala=c,class,Classes");
1✔
414
        command.add("--kinddef-scala=o,object,Objects");
1✔
415
        command.add("--kinddef-scala=C,caseClass,Case\\ classes");
1✔
416
        command.add("--kinddef-scala=O,caseObject,Case\\ objects");
1✔
417
        command.add("--kinddef-scala=t,trait,Traits");
1✔
418
        command.add("--kinddef-scala=m,method,Methods");
1✔
419
        command.add("--kinddef-scala=l,constant,Constants");
1✔
420
        command.add("--kinddef-scala=v,variable,Variables");
1✔
421
        command.add("--kinddef-scala=T,type,Types");
1✔
422
        command.add("--kinddef-scala=p,package,Packages");
1✔
423

424
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
425
                "(private|protected)?[[:space:]]*class[[:space:]]+([a-zA-Z0-9_]+)/\\4/c/");
426
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
427
                "(private|protected)?[[:space:]]*object[[:space:]]+([a-zA-Z0-9_]+)/\\4/o/");
428
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
429
                "(private|protected)?[[:space:]]*case class[[:space:]]+([a-zA-Z0-9_]+)/\\4/C/");
430
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
431
                "(private|protected)?[[:space:]]*case object[[:space:]]+([a-zA-Z0-9_]+)/\\4/O/");
432
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
433
                "(private|protected)?[[:space:]]*trait[[:space:]]+([a-zA-Z0-9_]+)/\\4/t/");
434
        command.add("--regex-scala=/^[[:space:]]*type[[:space:]]+([a-zA-Z0-9_]+)/\\1/T/");
1✔
435
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy|private|protected)" +
1✔
436
                "[[:space:]]*)*def[[:space:]]+([a-zA-Z0-9_]+)/\\3/m/");
437
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
438
                "val[[:space:]]+([a-zA-Z0-9_]+)/\\3/l/");
439
        command.add("--regex-scala=/^[[:space:]]*((abstract|final|sealed|implicit|lazy)[[:space:]]*)*" +
1✔
440
                "var[[:space:]]+([a-zA-Z0-9_]+)/\\3/v/");
441
        command.add("--regex-scala=/^[[:space:]]*package[[:space:]]+([a-zA-Z0-9_.]+)/\\1/p/");
1✔
442
    }
1✔
443

444
    /**
445
     * Run ctags on a file.
446
     * @param file file path to process
447
     * @return valid instance of {@link Definitions} or {@code null} on error
448
     * @throws IOException I/O exception
449
     * @throws InterruptedException interrupted command
450
     */
451
    @Nullable
452
    public Definitions doCtags(String file) throws IOException, InterruptedException {
453

454
        if (file.length() < 1 || "\n".equals(file)) {
1✔
455
            return null;
×
456
        }
457

458
        if (ctagsProcess != null) {
1✔
459
            try {
460
                int exitValue = ctagsProcess.exitValue();
×
461
                // If it is possible to retrieve exit value without exception
462
                // this means the ctags process is dead.
463
                LOGGER.log(Level.WARNING, "Ctags process exited with exit value {0}",
×
464
                        exitValue);
×
465
                // Throw the following to indicate non-I/O error for retry.
466
                throw new InterruptedException("ctags process died");
×
467
            } catch (IllegalThreadStateException exp) {
1✔
468
                // The ctags process is still running.
469
            }
1✔
470
        } else {
471
            initialize();
1✔
472
            run();
1✔
473
        }
474

475
        CtagsReader rdr = new CtagsReader();
1✔
476
        rdr.setSplitterSupplier(() -> trySplitSource(file));
1✔
477
        rdr.setTabSize(tabSize);
1✔
478
        Definitions ret = null;
1✔
479
        try {
480
            ctagsIn.write(file + "\n");
1✔
481
            if (Thread.interrupted()) {
1✔
482
                throw new InterruptedException("write()");
×
483
            }
484
            ctagsIn.flush();
1✔
485
            if (Thread.interrupted()) {
1✔
486
                throw new InterruptedException("flush()");
×
487
            }
488

489
            /*
490
             * Run the ctags reader in a time bound thread to make sure
491
             * the ctags process completes so that the indexer can
492
             * make progress instead of hanging the whole operation.
493
             */
494
            IndexerParallelizer parallelizer = env.getIndexerParallelizer();
1✔
495
            ExecutorService executor = parallelizer.getCtagsWatcherExecutor();
1✔
496
            Future<Definitions> future = executor.submit(() -> {
1✔
497
                readTags(rdr);
1✔
498
                return rdr.getDefinitions();
1✔
499
            });
500

501
            try {
502
                ret = future.get(getTimeout(), TimeUnit.SECONDS);
1✔
UNCOV
503
            } catch (ExecutionException ex) {
×
UNCOV
504
                LOGGER.log(Level.WARNING, "execution exception", ex);
×
505
            } catch (TimeoutException ex) {
×
506
                LOGGER.log(Level.WARNING,
×
507
                        String.format("Terminating ctags process for file '%s' " +
×
508
                                "due to timeout %d seconds", file, getTimeout()));
×
509
                close();
×
510
                // Allow for retry in IndexDatabase.
511
                throw new InterruptedException("ctags timeout");
×
512
            }
1✔
513
        } catch (IOException ex) {
1✔
514
            /*
515
             * In case the ctags process had to be destroyed, possibly pre-empt
516
             * the IOException with an InterruptedException.
517
             */
518
            if (Thread.interrupted()) {
1✔
519
                throw new InterruptedException("I/O");
×
520
            }
521
            throw ex;
1✔
522
        }
1✔
523

524
        return ret;
1✔
525
    }
526

527
    /**
528
     * Produce definitions for the text in the buffer String. ctags process is
529
     * mocked, not real mostly used for junit testing
530
     *
531
     * @param bufferTags tags file output
532
     * @return definitions parsed from buffer
533
     * @throws java.lang.InterruptedException interrupted
534
     */
535
    Definitions testCtagsParser(String bufferTags)
536
            throws InterruptedException {
537

538
        // Ensure output is magic-terminated as expected.
539
        StringBuilder tagsBuilder = new StringBuilder(bufferTags);
1✔
540
        if (!bufferTags.endsWith("\n")) {
1✔
541
            tagsBuilder.append("\n");
1✔
542
        }
543
        tagsBuilder.append(CTAGS_FILTER_TERMINATOR);
1✔
544
        tagsBuilder.append("\n");
1✔
545
        bufferTags = tagsBuilder.toString();
1✔
546

547
        junitTesting = true;
1✔
548
        ctagsOut = new BufferedReader(new StringReader(bufferTags));
1✔
549
        ctagsProcess = new Process() {
1✔
550
            @Override
551
            public OutputStream getOutputStream() {
552
                return null;
×
553
            }
554

555
            @Override
556
            public InputStream getInputStream() {
557
                return null;
×
558
            }
559

560
            @Override
561
            public InputStream getErrorStream() {
562
                return null;
×
563
            }
564

565
            @Override
566
            public int waitFor() {
567
                return 0;
×
568
            }
569

570
            @Override
571
            public int exitValue() {
572
                return 0;
×
573
            }
574

575
            @Override
576
            public void destroy() {
577
            }
×
578
        };
579

580
        CtagsReader rdr = new CtagsReader();
1✔
581
        rdr.setTabSize(tabSize);
1✔
582
        readTags(rdr);
1✔
583
        return rdr.getDefinitions();
1✔
584
    }
585

586
    private void readTags(CtagsReader reader) throws InterruptedException {
587
        try {
588
            do {
589
                String tagLine = ctagsOut.readLine();
1✔
590
                if (Thread.interrupted()) {
1✔
591
                    throw new InterruptedException("readLine()");
×
592
                }
593

594
                if (tagLine == null) {
1✔
UNCOV
595
                    if (!junitTesting) {
×
UNCOV
596
                        LOGGER.warning("ctags: Unexpected end of file!");
×
597
                    }
598
                    try {
UNCOV
599
                        int val = ctagsProcess.exitValue();
×
UNCOV
600
                        if (!junitTesting) {
×
UNCOV
601
                            LOGGER.log(Level.WARNING, "ctags exited with code: {0}", val);
×
602
                        }
603
                    } catch (IllegalThreadStateException e) {
×
604
                        LOGGER.log(Level.WARNING, "ctags EOF but did not exit");
×
605
                        ctagsProcess.destroyForcibly();
×
606
                    } catch (Exception e) {
×
607
                        LOGGER.log(Level.WARNING, "ctags problem:", e);
×
608
                        ctagsProcess.destroyForcibly();
×
UNCOV
609
                    }
×
610
                    // Throw the following to indicate non-I/O error for retry.
UNCOV
611
                    throw new InterruptedException("tagLine == null");
×
612
                }
613

614
                if (CTAGS_FILTER_TERMINATOR.equals(tagLine)) {
1✔
615
                    return;
1✔
616
                }
617

618
                //fix for bug #16334
619
                if (tagLine.endsWith(CTAGS_FILTER_TERMINATOR)) {
1✔
620
                    LOGGER.log(Level.WARNING, "ctags encountered a problem while generating tags for the file. The index will be incomplete.");
×
621
                    return;
×
622
                }
623

624
                reader.readLine(tagLine);
1✔
625
            } while (true);
1✔
UNCOV
626
        } catch (InterruptedException e) {
×
UNCOV
627
            throw e;
×
628
        } catch (Exception e) {
×
629
            LOGGER.log(Level.WARNING, "CTags parsing problem: ", e);
×
630
        }
631
        LOGGER.severe("CTag reader cycle was interrupted!");
×
632
    }
×
633

634
    /**
635
     * Attempts to create a {@link SourceSplitter} instance with content from
636
     * the specified file.
637
     * @return a defined instance or {@code null} on failure (without exception)
638
     */
639
    private static SourceSplitter trySplitSource(String filename) {
640
        SourceSplitter splitter = new SourceSplitter();
1✔
641
        try {
642
            StreamSource src = StreamSource.fromFile(new File(filename));
1✔
643
            splitter.reset(src);
1✔
644
        } catch (NullPointerException | IOException ex) {
×
645
            LOGGER.log(Level.WARNING, "Failed to re-read {0}", filename);
×
646
            return null;
×
647
        }
1✔
648
        LOGGER.log(Level.FINEST, "Re-read {0}", filename);
1✔
649
        return splitter;
1✔
650
    }
651
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc