llvm-project

Форк
0
667 строк · 23.7 Кб
1
#!/usr/bin/env python3
2
#
3
# ===- add_new_check.py - clang-tidy check generator ---------*- python -*--===#
4
#
5
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6
# See https://llvm.org/LICENSE.txt for license information.
7
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8
#
9
# ===-----------------------------------------------------------------------===#
10

11
from __future__ import print_function
12
from __future__ import unicode_literals
13

14
import argparse
15
import io
16
import os
17
import re
18
import sys
19

20
# Adapts the module's CMakelist file. Returns 'True' if it could add a new
21
# entry and 'False' if the entry already existed.
22
def adapt_cmake(module_path, check_name_camel):
23
    filename = os.path.join(module_path, "CMakeLists.txt")
24

25
    # The documentation files are encoded using UTF-8, however on Windows the
26
    # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is
27
    # always used, use `io.open(filename, mode, encoding='utf8')` for reading and
28
    # writing files here and elsewhere.
29
    with io.open(filename, "r", encoding="utf8") as f:
30
        lines = f.readlines()
31

32
    cpp_file = check_name_camel + ".cpp"
33

34
    # Figure out whether this check already exists.
35
    for line in lines:
36
        if line.strip() == cpp_file:
37
            return False
38

39
    print("Updating %s..." % filename)
40
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
41
        cpp_found = False
42
        file_added = False
43
        for line in lines:
44
            cpp_line = line.strip().endswith(".cpp")
45
            if (not file_added) and (cpp_line or cpp_found):
46
                cpp_found = True
47
                if (line.strip() > cpp_file) or (not cpp_line):
48
                    f.write("  " + cpp_file + "\n")
49
                    file_added = True
50
            f.write(line)
51

52
    return True
53

54

55
# Adds a header for the new check.
56
def write_header(module_path, module, namespace, check_name, check_name_camel):
57
    filename = os.path.join(module_path, check_name_camel) + ".h"
58
    print("Creating %s..." % filename)
59
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
60
        header_guard = (
61
            "LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_"
62
            + module.upper()
63
            + "_"
64
            + check_name_camel.upper()
65
            + "_H"
66
        )
67
        f.write("//===--- ")
68
        f.write(os.path.basename(filename))
69
        f.write(" - clang-tidy ")
70
        f.write("-" * max(0, 42 - len(os.path.basename(filename))))
71
        f.write("*- C++ -*-===//")
72
        f.write(
73
            """
74
//
75
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
76
// See https://llvm.org/LICENSE.txt for license information.
77
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
78
//
79
//===----------------------------------------------------------------------===//
80

81
#ifndef %(header_guard)s
82
#define %(header_guard)s
83

84
#include "../ClangTidyCheck.h"
85

86
namespace clang::tidy::%(namespace)s {
87

88
/// FIXME: Write a short description.
89
///
90
/// For the user-facing documentation see:
91
/// http://clang.llvm.org/extra/clang-tidy/checks/%(module)s/%(check_name)s.html
92
class %(check_name_camel)s : public ClangTidyCheck {
93
public:
94
  %(check_name_camel)s(StringRef Name, ClangTidyContext *Context)
95
      : ClangTidyCheck(Name, Context) {}
96
  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
97
  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
98
};
99

100
} // namespace clang::tidy::%(namespace)s
101

102
#endif // %(header_guard)s
103
"""
104
            % {
105
                "header_guard": header_guard,
106
                "check_name_camel": check_name_camel,
107
                "check_name": check_name,
108
                "module": module,
109
                "namespace": namespace,
110
            }
111
        )
112

113

114
# Adds the implementation of the new check.
115
def write_implementation(module_path, module, namespace, check_name_camel):
116
    filename = os.path.join(module_path, check_name_camel) + ".cpp"
117
    print("Creating %s..." % filename)
118
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
119
        f.write("//===--- ")
120
        f.write(os.path.basename(filename))
121
        f.write(" - clang-tidy ")
122
        f.write("-" * max(0, 51 - len(os.path.basename(filename))))
123
        f.write("-===//")
124
        f.write(
125
            """
126
//
127
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
128
// See https://llvm.org/LICENSE.txt for license information.
129
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
130
//
131
//===----------------------------------------------------------------------===//
132

133
#include "%(check_name)s.h"
134
#include "clang/ASTMatchers/ASTMatchFinder.h"
135

136
using namespace clang::ast_matchers;
137

138
namespace clang::tidy::%(namespace)s {
139

140
void %(check_name)s::registerMatchers(MatchFinder *Finder) {
141
  // FIXME: Add matchers.
142
  Finder->addMatcher(functionDecl().bind("x"), this);
143
}
144

145
void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
146
  // FIXME: Add callback implementation.
147
  const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
148
  if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().starts_with("awesome_"))
149
    return;
150
  diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
151
      << MatchedDecl
152
      << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
153
  diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note);
154
}
155

156
} // namespace clang::tidy::%(namespace)s
157
"""
158
            % {"check_name": check_name_camel, "module": module, "namespace": namespace}
159
        )
160

161

162
# Returns the source filename that implements the module.
163
def get_module_filename(module_path, module):
164
    modulecpp = list(
165
        filter(
166
            lambda p: p.lower() == module.lower() + "tidymodule.cpp",
167
            os.listdir(module_path),
168
        )
169
    )[0]
170
    return os.path.join(module_path, modulecpp)
171

172

173
# Modifies the module to include the new check.
174
def adapt_module(module_path, module, check_name, check_name_camel):
175
    filename = get_module_filename(module_path, module)
176
    with io.open(filename, "r", encoding="utf8") as f:
177
        lines = f.readlines()
178

179
    print("Updating %s..." % filename)
180
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
181
        header_added = False
182
        header_found = False
183
        check_added = False
184
        check_fq_name = module + "-" + check_name
185
        check_decl = (
186
            "    CheckFactories.registerCheck<"
187
            + check_name_camel
188
            + '>(\n        "'
189
            + check_fq_name
190
            + '");\n'
191
        )
192

193
        lines = iter(lines)
194
        try:
195
            while True:
196
                line = next(lines)
197
                if not header_added:
198
                    match = re.search('#include "(.*)"', line)
199
                    if match:
200
                        header_found = True
201
                        if match.group(1) > check_name_camel:
202
                            header_added = True
203
                            f.write('#include "' + check_name_camel + '.h"\n')
204
                    elif header_found:
205
                        header_added = True
206
                        f.write('#include "' + check_name_camel + '.h"\n')
207

208
                if not check_added:
209
                    if line.strip() == "}":
210
                        check_added = True
211
                        f.write(check_decl)
212
                    else:
213
                        match = re.search(
214
                            r'registerCheck<(.*)> *\( *(?:"([^"]*)")?', line
215
                        )
216
                        prev_line = None
217
                        if match:
218
                            current_check_name = match.group(2)
219
                            if current_check_name is None:
220
                                # If we didn't find the check name on this line, look on the
221
                                # next one.
222
                                prev_line = line
223
                                line = next(lines)
224
                                match = re.search(' *"([^"]*)"', line)
225
                                if match:
226
                                    current_check_name = match.group(1)
227
                            if current_check_name > check_fq_name:
228
                                check_added = True
229
                                f.write(check_decl)
230
                            if prev_line:
231
                                f.write(prev_line)
232
                f.write(line)
233
        except StopIteration:
234
            pass
235

236

237
# Adds a release notes entry.
238
def add_release_notes(module_path, module, check_name):
239
    check_name_dashes = module + "-" + check_name
240
    filename = os.path.normpath(
241
        os.path.join(module_path, "../../docs/ReleaseNotes.rst")
242
    )
243
    with io.open(filename, "r", encoding="utf8") as f:
244
        lines = f.readlines()
245

246
    lineMatcher = re.compile("New checks")
247
    nextSectionMatcher = re.compile("New check aliases")
248
    checkMatcher = re.compile("- New :doc:`(.*)")
249

250
    print("Updating %s..." % filename)
251
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
252
        note_added = False
253
        header_found = False
254
        add_note_here = False
255

256
        for line in lines:
257
            if not note_added:
258
                match = lineMatcher.match(line)
259
                match_next = nextSectionMatcher.match(line)
260
                match_check = checkMatcher.match(line)
261
                if match_check:
262
                    last_check = match_check.group(1)
263
                    if last_check > check_name_dashes:
264
                        add_note_here = True
265

266
                if match_next:
267
                    add_note_here = True
268

269
                if match:
270
                    header_found = True
271
                    f.write(line)
272
                    continue
273

274
                if line.startswith("^^^^"):
275
                    f.write(line)
276
                    continue
277

278
                if header_found and add_note_here:
279
                    if not line.startswith("^^^^"):
280
                        f.write(
281
                            """- New :doc:`%s
282
  <clang-tidy/checks/%s/%s>` check.
283

284
  FIXME: add release notes.
285

286
"""
287
                            % (check_name_dashes, module, check_name)
288
                        )
289
                        note_added = True
290

291
            f.write(line)
292

293

294
# Adds a test for the check.
295
def write_test(module_path, module, check_name, test_extension):
296
    check_name_dashes = module + "-" + check_name
297
    filename = os.path.normpath(
298
        os.path.join(
299
            module_path,
300
            "..",
301
            "..",
302
            "test",
303
            "clang-tidy",
304
            "checkers",
305
            module,
306
            check_name + "." + test_extension,
307
        )
308
    )
309
    print("Creating %s..." % filename)
310
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
311
        f.write(
312
            """// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
313

314
// FIXME: Add something that triggers the check here.
315
void f();
316
// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
317

318
// FIXME: Verify the applied fix.
319
//   * Make the CHECK patterns specific enough and try to make verified lines
320
//     unique to avoid incorrect matches.
321
//   * Use {{}} for regular expressions.
322
// CHECK-FIXES: {{^}}void awesome_f();{{$}}
323

324
// FIXME: Add something that doesn't trigger the check here.
325
void awesome_f2();
326
"""
327
            % {"check_name_dashes": check_name_dashes}
328
        )
329

330

331
def get_actual_filename(dirname, filename):
332
    if not os.path.isdir(dirname):
333
        return ""
334
    name = os.path.join(dirname, filename)
335
    if os.path.isfile(name):
336
        return name
337
    caselessname = filename.lower()
338
    for file in os.listdir(dirname):
339
        if file.lower() == caselessname:
340
            return os.path.join(dirname, file)
341
    return ""
342

343

344
# Recreates the list of checks in the docs/clang-tidy/checks directory.
345
def update_checks_list(clang_tidy_path):
346
    docs_dir = os.path.join(clang_tidy_path, "../docs/clang-tidy/checks")
347
    filename = os.path.normpath(os.path.join(docs_dir, "list.rst"))
348
    # Read the content of the current list.rst file
349
    with io.open(filename, "r", encoding="utf8") as f:
350
        lines = f.readlines()
351
    # Get all existing docs
352
    doc_files = []
353
    for subdir in filter(
354
        lambda s: os.path.isdir(os.path.join(docs_dir, s)), os.listdir(docs_dir)
355
    ):
356
        for file in filter(
357
            lambda s: s.endswith(".rst"), os.listdir(os.path.join(docs_dir, subdir))
358
        ):
359
            doc_files.append([subdir, file])
360
    doc_files.sort()
361

362
    # We couldn't find the source file from the check name, so try to find the
363
    # class name that corresponds to the check in the module file.
364
    def filename_from_module(module_name, check_name):
365
        module_path = os.path.join(clang_tidy_path, module_name)
366
        if not os.path.isdir(module_path):
367
            return ""
368
        module_file = get_module_filename(module_path, module_name)
369
        if not os.path.isfile(module_file):
370
            return ""
371
        with io.open(module_file, "r") as f:
372
            code = f.read()
373
            full_check_name = module_name + "-" + check_name
374
            name_pos = code.find('"' + full_check_name + '"')
375
            if name_pos == -1:
376
                return ""
377
            stmt_end_pos = code.find(";", name_pos)
378
            if stmt_end_pos == -1:
379
                return ""
380
            stmt_start_pos = code.rfind(";", 0, name_pos)
381
            if stmt_start_pos == -1:
382
                stmt_start_pos = code.rfind("{", 0, name_pos)
383
            if stmt_start_pos == -1:
384
                return ""
385
            stmt = code[stmt_start_pos + 1 : stmt_end_pos]
386
            matches = re.search(r'registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt)
387
            if matches and matches[2] == full_check_name:
388
                class_name = matches[1]
389
                if "::" in class_name:
390
                    parts = class_name.split("::")
391
                    class_name = parts[-1]
392
                    class_path = os.path.join(
393
                        clang_tidy_path, module_name, "..", *parts[0:-1]
394
                    )
395
                else:
396
                    class_path = os.path.join(clang_tidy_path, module_name)
397
                return get_actual_filename(class_path, class_name + ".cpp")
398

399
        return ""
400

401
    # Examine code looking for a c'tor definition to get the base class name.
402
    def get_base_class(code, check_file):
403
        check_class_name = os.path.splitext(os.path.basename(check_file))[0]
404
        ctor_pattern = check_class_name + r"\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\("
405
        matches = re.search(r"\s+" + check_class_name + "::" + ctor_pattern, code)
406

407
        # The constructor might be inline in the header.
408
        if not matches:
409
            header_file = os.path.splitext(check_file)[0] + ".h"
410
            if not os.path.isfile(header_file):
411
                return ""
412
            with io.open(header_file, encoding="utf8") as f:
413
                code = f.read()
414
            matches = re.search(" " + ctor_pattern, code)
415

416
        if matches and matches[1] != "ClangTidyCheck":
417
            return matches[1]
418
        return ""
419

420
    # Some simple heuristics to figure out if a check has an autofix or not.
421
    def has_fixits(code):
422
        for needle in [
423
            "FixItHint",
424
            "ReplacementText",
425
            "fixit",
426
            "TransformerClangTidyCheck",
427
        ]:
428
            if needle in code:
429
                return True
430
        return False
431

432
    # Try to figure out of the check supports fixits.
433
    def has_auto_fix(check_name):
434
        dirname, _, check_name = check_name.partition("-")
435

436
        check_file = get_actual_filename(
437
            os.path.join(clang_tidy_path, dirname),
438
            get_camel_check_name(check_name) + ".cpp",
439
        )
440
        if not os.path.isfile(check_file):
441
            # Some older checks don't end with 'Check.cpp'
442
            check_file = get_actual_filename(
443
                os.path.join(clang_tidy_path, dirname),
444
                get_camel_name(check_name) + ".cpp",
445
            )
446
            if not os.path.isfile(check_file):
447
                # Some checks aren't in a file based on the check name.
448
                check_file = filename_from_module(dirname, check_name)
449
                if not check_file or not os.path.isfile(check_file):
450
                    return ""
451

452
        with io.open(check_file, encoding="utf8") as f:
453
            code = f.read()
454
            if has_fixits(code):
455
                return ' "Yes"'
456

457
        base_class = get_base_class(code, check_file)
458
        if base_class:
459
            base_file = os.path.join(clang_tidy_path, dirname, base_class + ".cpp")
460
            if os.path.isfile(base_file):
461
                with io.open(base_file, encoding="utf8") as f:
462
                    code = f.read()
463
                    if has_fixits(code):
464
                        return ' "Yes"'
465

466
        return ""
467

468
    def process_doc(doc_file):
469
        check_name = doc_file[0] + "-" + doc_file[1].replace(".rst", "")
470

471
        with io.open(os.path.join(docs_dir, *doc_file), "r", encoding="utf8") as doc:
472
            content = doc.read()
473
            match = re.search(".*:orphan:.*", content)
474

475
            if match:
476
                # Orphan page, don't list it.
477
                return "", ""
478

479
            match = re.search(r".*:http-equiv=refresh: \d+;URL=(.*).html(.*)", content)
480
            # Is it a redirect?
481
            return check_name, match
482

483
    def format_link(doc_file):
484
        check_name, match = process_doc(doc_file)
485
        if not match and check_name and not check_name.startswith("clang-analyzer-"):
486
            return "   :doc:`%(check_name)s <%(module)s/%(check)s>`,%(autofix)s\n" % {
487
                "check_name": check_name,
488
                "module": doc_file[0],
489
                "check": doc_file[1].replace(".rst", ""),
490
                "autofix": has_auto_fix(check_name),
491
            }
492
        else:
493
            return ""
494

495
    def format_link_alias(doc_file):
496
        check_name, match = process_doc(doc_file)
497
        if (match or (check_name.startswith("clang-analyzer-"))) and check_name:
498
            module = doc_file[0]
499
            check_file = doc_file[1].replace(".rst", "")
500
            if not match or match.group(1) == "https://clang.llvm.org/docs/analyzer/checkers":
501
                title = "Clang Static Analyzer " + check_file
502
                # Preserve the anchor in checkers.html from group 2.
503
                target = "" if not match else match.group(1) + ".html" + match.group(2)
504
                autofix = ""
505
                ref_begin = ""
506
                ref_end = "_"
507
            else:
508
                redirect_parts = re.search(r"^\.\./([^/]*)/([^/]*)$", match.group(1))
509
                title = redirect_parts[1] + "-" + redirect_parts[2]
510
                target = redirect_parts[1] + "/" + redirect_parts[2]
511
                autofix = has_auto_fix(title)
512
                ref_begin = ":doc:"
513
                ref_end = ""
514

515
            if target:
516
                # The checker is just a redirect.
517
                return (
518
                        "   :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(ref_begin)s`%(title)s <%(target)s>`%(ref_end)s,%(autofix)s\n"
519
                    % {
520
                        "check_name": check_name,
521
                        "module": module,
522
                        "check_file": check_file,
523
                        "target": target,
524
                        "title": title,
525
                        "autofix": autofix,
526
                        "ref_begin" : ref_begin,
527
                        "ref_end" : ref_end
528
                    })
529
            else:
530
                # The checker is just a alias without redirect.
531
                return (
532
                        "   :doc:`%(check_name)s <%(module)s/%(check_file)s>`, %(title)s,%(autofix)s\n"
533
                    % {
534
                        "check_name": check_name,
535
                        "module": module,
536
                        "check_file": check_file,
537
                        "target": target,
538
                        "title": title,
539
                        "autofix": autofix,
540
                    })
541
        return ""
542

543
    checks = map(format_link, doc_files)
544
    checks_alias = map(format_link_alias, doc_files)
545

546
    print("Updating %s..." % filename)
547
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
548
        for line in lines:
549
            f.write(line)
550
            if line.strip() == ".. csv-table::":
551
                # We dump the checkers
552
                f.write('   :header: "Name", "Offers fixes"\n\n')
553
                f.writelines(checks)
554
                # and the aliases
555
                f.write("\n\n")
556
                f.write(".. csv-table:: Aliases..\n")
557
                f.write('   :header: "Name", "Redirect", "Offers fixes"\n\n')
558
                f.writelines(checks_alias)
559
                break
560

561

562
# Adds a documentation for the check.
563
def write_docs(module_path, module, check_name):
564
    check_name_dashes = module + "-" + check_name
565
    filename = os.path.normpath(
566
        os.path.join(
567
            module_path, "../../docs/clang-tidy/checks/", module, check_name + ".rst"
568
        )
569
    )
570
    print("Creating %s..." % filename)
571
    with io.open(filename, "w", encoding="utf8", newline="\n") as f:
572
        f.write(
573
            """.. title:: clang-tidy - %(check_name_dashes)s
574

575
%(check_name_dashes)s
576
%(underline)s
577

578
FIXME: Describe what patterns does the check detect and why. Give examples.
579
"""
580
            % {
581
                "check_name_dashes": check_name_dashes,
582
                "underline": "=" * len(check_name_dashes),
583
            }
584
        )
585

586

587
def get_camel_name(check_name):
588
    return "".join(map(lambda elem: elem.capitalize(), check_name.split("-")))
589

590

591
def get_camel_check_name(check_name):
592
    return get_camel_name(check_name) + "Check"
593

594

595
def main():
596
    language_to_extension = {
597
        "c": "c",
598
        "c++": "cpp",
599
        "objc": "m",
600
        "objc++": "mm",
601
    }
602
    parser = argparse.ArgumentParser()
603
    parser.add_argument(
604
        "--update-docs",
605
        action="store_true",
606
        help="just update the list of documentation files, then exit",
607
    )
608
    parser.add_argument(
609
        "--language",
610
        help="language to use for new check (defaults to c++)",
611
        choices=language_to_extension.keys(),
612
        default="c++",
613
        metavar="LANG",
614
    )
615
    parser.add_argument(
616
        "module",
617
        nargs="?",
618
        help="module directory under which to place the new tidy check (e.g., misc)",
619
    )
620
    parser.add_argument(
621
        "check", nargs="?", help="name of new tidy check to add (e.g. foo-do-the-stuff)"
622
    )
623
    args = parser.parse_args()
624

625
    if args.update_docs:
626
        update_checks_list(os.path.dirname(sys.argv[0]))
627
        return
628

629
    if not args.module or not args.check:
630
        print("Module and check must be specified.")
631
        parser.print_usage()
632
        return
633

634
    module = args.module
635
    check_name = args.check
636
    check_name_camel = get_camel_check_name(check_name)
637
    if check_name.startswith(module):
638
        print(
639
            'Check name "%s" must not start with the module "%s". Exiting.'
640
            % (check_name, module)
641
        )
642
        return
643
    clang_tidy_path = os.path.dirname(sys.argv[0])
644
    module_path = os.path.join(clang_tidy_path, module)
645

646
    if not adapt_cmake(module_path, check_name_camel):
647
        return
648

649
    # Map module names to namespace names that don't conflict with widely used top-level namespaces.
650
    if module == "llvm":
651
        namespace = module + "_check"
652
    else:
653
        namespace = module
654

655
    write_header(module_path, module, namespace, check_name, check_name_camel)
656
    write_implementation(module_path, module, namespace, check_name_camel)
657
    adapt_module(module_path, module, check_name, check_name_camel)
658
    add_release_notes(module_path, module, check_name)
659
    test_extension = language_to_extension.get(args.language)
660
    write_test(module_path, module, check_name, test_extension)
661
    write_docs(module_path, module, check_name)
662
    update_checks_list(clang_tidy_path)
663
    print("Done. Now it's your turn!")
664

665

666
if __name__ == "__main__":
667
    main()
668

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.