diff --git a/.ci/compute_projects.py b/.ci/compute_projects.py index c3cf714ce6c10..2dc5629328457 100644 --- a/.ci/compute_projects.py +++ b/.ci/compute_projects.py @@ -19,6 +19,7 @@ PROJECT_DEPENDENCIES = { "llvm": set(), "clang": {"llvm"}, + "CIR": {"clang", "mlir"}, "bolt": {"clang", "lld", "llvm"}, "clang-tools-extra": {"clang", "llvm"}, "compiler-rt": {"clang", "lld"}, @@ -55,6 +56,7 @@ ".ci": { "llvm", "clang", + "CIR", "lld", "lldb", "bolt", @@ -128,6 +130,7 @@ "lldb": "check-lldb", "llvm": "check-llvm", "clang": "check-clang", + "CIR": "check-clang-cir", "bolt": "check-bolt", "lld": "check-lld", "flang": "check-flang", @@ -141,6 +144,23 @@ RUNTIMES = {"libcxx", "libcxxabi", "libunwind", "compiler-rt", "libc"} +# Meta projects are projects that need explicit handling but do not reside +# in their own top level folder. To add a meta project, the start of the path +# for the metaproject should be mapped to the name of the project below. +# Multiple paths can map to the same metaproject. +META_PROJECTS = { + ("clang", "lib", "CIR"): "CIR", + ("clang", "test", "CIR"): "CIR", + ("clang", "include", "clang", "CIR"): "CIR", + ("*", "docs"): "docs", + ("llvm", "utils", "gn"): "gn", + (".github", "workflows", "premerge.yaml"): ".ci", + ("third-party",): ".ci", +} + +# Projects that should not run any tests. These need to be metaprojects. +SKIP_PROJECTS = ["docs", "gn"] + def _add_dependencies(projects: Set[str], runtimes: Set[str]) -> Set[str]: projects_with_dependents = set(projects) @@ -233,21 +253,34 @@ def _compute_runtimes_to_build( return _exclude_projects(runtimes_to_build, platform) +def _path_matches(matcher: tuple[str], file_path: tuple[str]) -> bool: + if len(file_path) < len(matcher): + return False + for match_part, file_part in zip(matcher, file_path): + if match_part == "*" or file_part == "*": + continue + if match_part != file_part: + return False + return True + + +def _get_modified_projects_for_file(modified_file: str) -> Set[str]: + modified_projects = set() + path_parts = pathlib.Path(modified_file).parts + for meta_project_files in META_PROJECTS.keys(): + if _path_matches(meta_project_files, path_parts): + meta_project = META_PROJECTS[meta_project_files] + if meta_project in SKIP_PROJECTS: + return set() + modified_projects.add(meta_project) + modified_projects.add(pathlib.Path(modified_file).parts[0]) + return modified_projects + + def _get_modified_projects(modified_files: list[str]) -> Set[str]: modified_projects = set() for modified_file in modified_files: - path_parts = pathlib.Path(modified_file).parts - # Exclude files in the docs directory. They do not impact an test - # targets and there is a separate workflow used for ensuring the - # documentation builds. - if len(path_parts) > 2 and path_parts[1] == "docs": - continue - # Exclude files for the gn build. We do not test it within premerge - # and changes occur often enough that they otherwise take up - # capacity. - if len(path_parts) > 3 and path_parts[:3] == ("llvm", "utils", "gn"): - continue - modified_projects.add(pathlib.Path(modified_file).parts[0]) + modified_projects.update(_get_modified_projects_for_file(modified_file)) return modified_projects @@ -267,6 +300,13 @@ def get_env_variables(modified_files: list[str], platform: str) -> Set[str]: runtimes_check_targets_needs_reconfig = _compute_project_check_targets( runtimes_to_test_needs_reconfig ) + + # CIR is used as a pseudo-project in this script. It is built as part of the + # clang build, but it requires an explicit option to enable. We set that + # option here, and remove it from the projects_to_build list. + enable_cir = "ON" if "CIR" in projects_to_build else "OFF" + projects_to_build.discard("CIR") + # We use a semicolon to separate the projects/runtimes as they get passed # to the CMake invocation and thus we need to use the CMake list separator # (;). We use spaces to separate the check targets as they end up getting @@ -279,6 +319,7 @@ def get_env_variables(modified_files: list[str], platform: str) -> Set[str]: "runtimes_check_targets_needs_reconfig": " ".join( sorted(runtimes_check_targets_needs_reconfig) ), + "enable_cir": enable_cir, } diff --git a/.ci/compute_projects_test.py b/.ci/compute_projects_test.py index 6299931e1ec34..11c4aea9b4e35 100644 --- a/.ci/compute_projects_test.py +++ b/.ci/compute_projects_test.py @@ -1,7 +1,7 @@ # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. # See https://llvm.org/LICENSE.txt for license information. # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -"""Does some stuff.""" +"""Tests for compute_projects.py""" import unittest @@ -104,6 +104,10 @@ def test_clang(self): env_variables["runtimes_check_targets_needs_reconfig"], "check-cxx check-cxxabi check-unwind", ) + self.assertEqual( + env_variables["enable_cir"], + "OFF", + ) def test_clang_windows(self): env_variables = compute_projects.get_env_variables( @@ -126,6 +130,32 @@ def test_clang_windows(self): env_variables["runtimes_check_targets_needs_reconfig"], "check-cxx check-cxxabi check-unwind", ) + self.assertEqual(env_variables["enable_cir"], "OFF") + + def test_cir(self): + env_variables = compute_projects.get_env_variables( + ["clang/lib/CIR/CMakeLists.txt"], "Linux" + ) + self.assertEqual( + env_variables["projects_to_build"], + "clang;clang-tools-extra;lld;llvm;mlir", + ) + self.assertEqual( + env_variables["project_check_targets"], + "check-clang check-clang-cir check-clang-tools", + ) + self.assertEqual( + env_variables["runtimes_to_build"], "compiler-rt;libcxx;libcxxabi;libunwind" + ) + self.assertEqual( + env_variables["runtimes_check_targets"], + "check-compiler-rt", + ) + self.assertEqual( + env_variables["runtimes_check_targets_needs_reconfig"], + "check-cxx check-cxxabi check-unwind", + ) + self.assertEqual(env_variables["enable_cir"], "ON") def test_bolt(self): env_variables = compute_projects.get_env_variables( @@ -158,6 +188,7 @@ def test_mlir(self): self.assertEqual(env_variables["runtimes_to_build"], "") self.assertEqual(env_variables["runtimes_check_targets"], "") self.assertEqual(env_variables["runtimes_check_targets_needs_reconfig"], "") + self.assertEqual(env_variables["enable_cir"], "OFF") def test_flang(self): env_variables = compute_projects.get_env_variables( @@ -168,10 +199,11 @@ def test_flang(self): self.assertEqual(env_variables["runtimes_to_build"], "") self.assertEqual(env_variables["runtimes_check_targets"], "") self.assertEqual(env_variables["runtimes_check_targets_needs_reconfig"], "") + self.assertEqual(env_variables["enable_cir"], "OFF") def test_invalid_subproject(self): env_variables = compute_projects.get_env_variables( - ["third-party/benchmark/CMakeLists.txt"], "Linux" + ["llvm-libgcc/CMakeLists.txt"], "Linux" ) self.assertEqual(env_variables["projects_to_build"], "") self.assertEqual(env_variables["project_check_targets"], "") @@ -237,7 +269,7 @@ def test_ci(self): ) self.assertEqual( env_variables["project_check_targets"], - "check-bolt check-clang check-clang-tools check-flang check-lld check-lldb check-llvm check-mlir check-polly", + "check-bolt check-clang check-clang-cir check-clang-tools check-flang check-lld check-lldb check-llvm check-mlir check-polly", ) self.assertEqual( env_variables["runtimes_to_build"], @@ -276,6 +308,66 @@ def test_clang_tools_extra(self): self.assertEqual(env_variables["runtimes_check_targets"], "check-libc") self.assertEqual(env_variables["runtimes_check_targets_needs_reconfig"], "") + def test_premerge_workflow(self): + env_variables = compute_projects.get_env_variables( + [".github/workflows/premerge.yaml"], "Linux" + ) + self.assertEqual( + env_variables["projects_to_build"], + "bolt;clang;clang-tools-extra;flang;libclc;lld;lldb;llvm;mlir;polly", + ) + self.assertEqual( + env_variables["project_check_targets"], + "check-bolt check-clang check-clang-cir check-clang-tools check-flang check-lld check-lldb check-llvm check-mlir check-polly", + ) + self.assertEqual( + env_variables["runtimes_to_build"], + "compiler-rt;libc;libcxx;libcxxabi;libunwind", + ) + self.assertEqual( + env_variables["runtimes_check_targets"], + "check-compiler-rt check-libc", + ) + self.assertEqual( + env_variables["runtimes_check_targets_needs_reconfig"], + "check-cxx check-cxxabi check-unwind", + ) + + def test_other_github_workflow(self): + env_variables = compute_projects.get_env_variables( + [".github/workflows/docs.yml"], "Linux" + ) + self.assertEqual(env_variables["projects_to_build"], "") + self.assertEqual(env_variables["project_check_targets"], "") + self.assertEqual(env_variables["runtimes_to_build"], "") + self.assertEqual(env_variables["runtimes_check_targets"], "") + self.assertEqual(env_variables["runtimes_check_targets_needs_reconfig"], "") + + def test_third_party_benchmark(self): + env_variables = compute_projects.get_env_variables( + ["third-party/benchmark/CMakeLists.txt"], "Linux" + ) + self.assertEqual( + env_variables["projects_to_build"], + "bolt;clang;clang-tools-extra;flang;libclc;lld;lldb;llvm;mlir;polly", + ) + self.assertEqual( + env_variables["project_check_targets"], + "check-bolt check-clang check-clang-cir check-clang-tools check-flang check-lld check-lldb check-llvm check-mlir check-polly", + ) + self.assertEqual( + env_variables["runtimes_to_build"], + "compiler-rt;libc;libcxx;libcxxabi;libunwind", + ) + self.assertEqual( + env_variables["runtimes_check_targets"], + "check-compiler-rt check-libc", + ) + self.assertEqual( + env_variables["runtimes_check_targets_needs_reconfig"], + "check-cxx check-cxxabi check-unwind", + ) + if __name__ == "__main__": unittest.main() diff --git a/.ci/metrics/metrics.py b/.ci/metrics/metrics.py index 143e6ab4cf46a..26fdeef1913ab 100644 --- a/.ci/metrics/metrics.py +++ b/.ci/metrics/metrics.py @@ -1,3 +1,13 @@ +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Collects Github metrics and uploads them to Grafana. + +This script contains machinery that will pull metrics periodically from Github +about workflow runs. It will upload the collected metrics to the specified +Grafana instance. +""" + import collections import datetime import github diff --git a/.ci/metrics/metrics_test.py b/.ci/metrics/metrics_test.py new file mode 100644 index 0000000000000..259e55f817939 --- /dev/null +++ b/.ci/metrics/metrics_test.py @@ -0,0 +1,75 @@ +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +"""Tests for metrics.py""" + +from dataclasses import dataclass +import requests +import unittest +import unittest.mock + +import metrics + + +class TestMetrics(unittest.TestCase): + def test_upload_gauge_metric(self): + """Test that we can upload a gauge metric correctly. + + Also verify that we pass around parameters like API keys and user IDs + correctly to the HTTP POST request. + """ + test_metrics = [metrics.GaugeMetric("gauge_test", 5, 1000)] + return_value = requests.Response() + return_value.status_code = 204 + with unittest.mock.patch( + "requests.post", return_value=return_value + ) as post_mock: + metrics.upload_metrics(test_metrics, "test_userid", "test_api_key") + self.assertSequenceEqual(post_mock.call_args.args, [metrics.GRAFANA_URL]) + self.assertEqual( + post_mock.call_args.kwargs["data"], "gauge_test value=5 1000" + ) + self.assertEqual( + post_mock.call_args.kwargs["auth"], ("test_userid", "test_api_key") + ) + + def test_upload_job_metric(self): + """Test that we can upload a job metric correctly.""" + test_metrics = [ + metrics.JobMetrics("test_job", 5, 10, 1, 1000, 7, "test_workflow") + ] + return_value = requests.Response() + return_value.status_code = 204 + with unittest.mock.patch( + "requests.post", return_value=return_value + ) as post_mock: + metrics.upload_metrics(test_metrics, "test_userid", "test_aoi_key") + self.assertEqual( + post_mock.call_args.kwargs["data"], + "test_job queue_time=5,run_time=10,status=1 1000", + ) + + def test_upload_unknown_metric(self): + """Test we report an error if we encounter an unknown metric type.""" + + @dataclass + class FakeMetric: + fake_data: str + + test_metrics = [FakeMetric("test")] + + with self.assertRaises(ValueError): + metrics.upload_metrics(test_metrics, "test_userid", "test_api_key") + + def test_bad_response_code(self): + """Test that we gracefully handle HTTP response errors.""" + test_metrics = [metrics.GaugeMetric("gauge_test", 5, 1000)] + return_value = requests.Response() + return_value.status_code = 403 + # Just assert that we continue running here and do not raise anything. + with unittest.mock.patch("requests.post", return_value=return_value) as _: + metrics.upload_metrics(test_metrics, "test_userid", "test_api_key") + + +if __name__ == "__main__": + unittest.main() diff --git a/.ci/monolithic-linux.sh b/.ci/monolithic-linux.sh index 8d1faab13986c..6db24d894eb73 100755 --- a/.ci/monolithic-linux.sh +++ b/.ci/monolithic-linux.sh @@ -21,12 +21,7 @@ BUILD_DIR="${BUILD_DIR:=${MONOREPO_ROOT}/build}" INSTALL_DIR="${BUILD_DIR}/install" rm -rf "${BUILD_DIR}" -ccache --zero-stats - -if [[ -n "${CLEAR_CACHE:-}" ]]; then - echo "clearing cache" - ccache --clear -fi +sccache --zero-stats mkdir -p artifacts/reproducers @@ -36,7 +31,7 @@ export CLANG_CRASH_DIAGNOSTICS_DIR=`realpath artifacts/reproducers` function at-exit { retcode=$? - ccache --print-stats > artifacts/ccache_stats.txt + sccache --show-stats > artifacts/sccache_stats.txt cp "${BUILD_DIR}"/.ninja_log artifacts/.ninja_log cp "${BUILD_DIR}"/test-results.*.xml artifacts/ || : @@ -53,6 +48,7 @@ targets="${2}" runtimes="${3}" runtime_targets="${4}" runtime_targets_needs_reconfig="${5}" +enable_cir="${6}" lit_args="-v --xunit-xml-output ${BUILD_DIR}/test-results.xml --use-unique-output-file-name --timeout=1200 --time-tests" @@ -72,13 +68,15 @@ cmake -S "${MONOREPO_ROOT}"/llvm -B "${BUILD_DIR}" \ -G Ninja \ -D CMAKE_PREFIX_PATH="${HOME}/.local" \ -D CMAKE_BUILD_TYPE=Release \ + -D CLANG_ENABLE_CIR=${enable_cir} \ -D LLVM_ENABLE_ASSERTIONS=ON \ -D LLVM_BUILD_EXAMPLES=ON \ -D COMPILER_RT_BUILD_LIBFUZZER=OFF \ -D LLVM_LIT_ARGS="${lit_args}" \ -D LLVM_ENABLE_LLD=ON \ -D CMAKE_CXX_FLAGS=-gmlt \ - -D LLVM_CCACHE_BUILD=ON \ + -D CMAKE_C_COMPILER_LAUNCHER=sccache \ + -D CMAKE_CXX_COMPILER_LAUNCHER=sccache \ -D LIBCXX_CXX_ABI=libcxxabi \ -D MLIR_ENABLE_BINDINGS_PYTHON=ON \ -D LLDB_ENABLE_PYTHON=ON \ diff --git a/.ci/monolithic-windows.sh b/.ci/monolithic-windows.sh index 176350fac604c..50a741677d734 100755 --- a/.ci/monolithic-windows.sh +++ b/.ci/monolithic-windows.sh @@ -21,11 +21,6 @@ BUILD_DIR="${BUILD_DIR:=${MONOREPO_ROOT}/build}" rm -rf "${BUILD_DIR}" -if [[ -n "${CLEAR_CACHE:-}" ]]; then - echo "clearing sccache" - rm -rf "$SCCACHE_DIR" -fi - sccache --zero-stats function at-exit { retcode=$? diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 01569c619f8f6..ea789ab5273a6 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -106,3 +106,40 @@ d33bf2e9df578ff7e44fd22504d6ad5a122b7ee6 ce46adb8b7ce645353eccaedf31ed9765dab77bb 68070f908bb7ac5f0b5fa9722caa504ecf723f6b 5213c57cb1f0d78aad9a253b7f6a2b62ff4c7859 + +# [mlir] Update create method +9e7834cadf48292b5d127d6d98f9e6d565ed5d9a +284a5c2c0b97edddf255ea210f939203ad3d09f2 +c090ed53fb73f59cf221f5610430af8047758117 +fcbcfe44cff00101a6a98a73971398eb8dd87710 +258daf539583b80e0217d1d87941412d65cf16aa +c610b244937ed847b0275ccb038c0f2d36310b4a +b58ad3650f2195117f484d551ffbada27e7d1e14 +258d04c810ab10f101324cbf1fe3c7be65eb1938 +a6bf40d1c6cf010b3ad90bf7f410983453f4deb2 +dcfc853c51aecf6538182378c016f8e1604e7e97 +3f74334c38120bbdefac012d478dfce8e4eb0906 +a636b7bfdd1d8304b78e8b42ec900a21736d4afb +75aa7065dcf653de7870758cd502a7c714f4bcd7 +2f5312563fd5cb2e355ec49109f3e63875337c7c +967626b842551ecd997c0d10eb68c3015b63a3d7 +588845defd09359a8b87db339b563af848cf45a7 +b0434925c98c9a8906afea60a1304c870b1f574a +8fff238b2c363b036ce9e7bf7abab3acafc87ab2 +38976a03cd367b27437e0d1e81c0ccaee2777b47 +eaa67a3cf041009ae33a45159d0465262c3af5dc +b0312be6aa664e4cb9abec6d080e971493093d05 +2736fbd8324bf21a130c8abd4bd0e7d3aa840ac1 +4ae9fdca8af095afd91705f8dd143e93b304b6fb +f904cdd6c3049e605d24ed17680e80e7133908a0 +972ac59c9af4ad47af0b3542ae936b3470727e5f +7b787965431e666858fdf66db25ee5a129833927 +c3823af156b517d926a56e3d0d585e2a15720e96 +dce6679cf5cbbdaffb9c2b51dc762c5c6689ea78 +9844ba6d9740206129b52633c555f767eaa45581 +5547c6cd03ddddd405a09e51624e1f19955a85b1 +a3a007ad5fa20abc90ead4e1030b481bf109b4cf +46f6df0848ea04449c6179ecdedc404ee5b5cf11 +b7e332d3f59f567b1999fbcc660d7837cba8e406 +6056f942abe83b05406df8b04e95ec37a3d160b5 +906295b8a31c8dac5aa845864c0bca9f02f86184 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c951a24fc8d31..23e13fdd060d3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,9 +31,9 @@ /clang/www/cxx_dr_status.html @Endilll /clang/www/make_cxx_dr_status @Endilll -/clang/include/clang/CIR @lanza @bcardosolopes -/clang/lib/CIR @lanza @bcardosolopes -/clang/tools/cir-* @lanza @bcardosolopes +/clang/include/clang/CIR @lanza @bcardosolopes @xlauko @andykaylor +/clang/lib/CIR @lanza @bcardosolopes @xlauko @andykaylor +/clang/tools/cir-* @lanza @bcardosolopes @xlauko @andykaylor /lldb/ @JDevlieghere @@ -120,8 +120,9 @@ /mlir/**/Index* @mogball # MLIR Python Bindings -/mlir/test/python/ @ftynse @makslevental @stellaraccident -/mlir/python/ @ftynse @makslevental @stellaraccident +/mlir/test/python/ @ftynse @makslevental @stellaraccident @rolfmorel +/mlir/python/ @ftynse @makslevental @stellaraccident @rolfmorel +/mlir/lib/Bindings/Python @makslevental @rolfmorel # MLIR Mem2Reg/SROA /mlir/**/Transforms/Mem2Reg.* @moxinilian diff --git a/.github/new-prs-labeler.yml b/.github/new-prs-labeler.yml index b05e9c6c56ed0..8e0fa8d42d735 100644 --- a/.github/new-prs-labeler.yml +++ b/.github/new-prs-labeler.yml @@ -48,6 +48,9 @@ flang:frontend: - flang/Evaluate/**/* - flang/Semantics/**/* +libclc: + - libclc/** + HLSL: - clang/*HLSL*/**/* - clang/**/*HLSL* @@ -717,6 +720,8 @@ mlgo: - llvm/lib/Analysis/IR2Vec.cpp - llvm/lib/Analysis/models/** - llvm/test/Analysis/IR2Vec/** + - llvm/tools/llvm-ir2vec/** + - llvm/docs/CommandGuide/llvm-ir2vec.rst tools:llvm-exegesis: - llvm/tools/llvm-exegesis/** diff --git a/.github/workflows/build-ci-container-windows.yml b/.github/workflows/build-ci-container-windows.yml index 59079f057d021..f76c69f29fb30 100644 --- a/.github/workflows/build-ci-container-windows.yml +++ b/.github/workflows/build-ci-container-windows.yml @@ -11,8 +11,6 @@ on: - .github/workflows/build-ci-container-windows.yml - '.github/workflows/containers/github-action-ci-windows/**' pull_request: - branches: - - main paths: - .github/workflows/build-ci-container-windows.yml - '.github/workflows/containers/github-action-ci-windows/**' diff --git a/.github/workflows/build-ci-container.yml b/.github/workflows/build-ci-container.yml index 3159aae32ca51..7f01264af8534 100644 --- a/.github/workflows/build-ci-container.yml +++ b/.github/workflows/build-ci-container.yml @@ -11,8 +11,6 @@ on: - .github/workflows/build-ci-container.yml - '.github/workflows/containers/github-action-ci/**' pull_request: - branches: - - main paths: - .github/workflows/build-ci-container.yml - '.github/workflows/containers/github-action-ci/**' diff --git a/.github/workflows/check-ci.yml b/.github/workflows/check-ci.yml new file mode 100644 index 0000000000000..befea2093f908 --- /dev/null +++ b/.github/workflows/check-ci.yml @@ -0,0 +1,38 @@ +name: Check CI Scripts + +permissions: + contents: read + +on: + push: + paths: + - '.ci/**' + - '.github/workflows/check-ci.yml' + pull_request: + paths: + - '.ci/**' + - '.github/workflows/check-ci.yml' + +jobs: + test-python: + name: "Check Python Tests" + runs-on: ubuntu-24.04 + if: github.repository == 'llvm/llvm-project' + steps: + - name: Fetch LLVM sources + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: .ci + - name: Setup Python + uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 + with: + python-version: 3.13 + cache: 'pip' + - name: Install Python Dependencies + run: | + pip3 install -r .ci/all_requirements.txt + pip3 install -r .ci/metrics/requirements.lock.txt + pip3 install pytest==8.4.1 + - name: Run Tests + working-directory: .ci + run: pytest diff --git a/.github/workflows/containers/github-action-ci-windows/Dockerfile b/.github/workflows/containers/github-action-ci-windows/Dockerfile index b909d14b4eeeb..8ee3a83f0e04a 100644 --- a/.github/workflows/containers/github-action-ci-windows/Dockerfile +++ b/.github/workflows/containers/github-action-ci-windows/Dockerfile @@ -90,7 +90,7 @@ RUN powershell -Command \ RUN git config --system core.longpaths true & \ git config --global core.autocrlf false -ARG RUNNER_VERSION=2.326.0 +ARG RUNNER_VERSION=2.327.0 ENV RUNNER_VERSION=$RUNNER_VERSION RUN powershell -Command \ diff --git a/.github/workflows/containers/github-action-ci/Dockerfile b/.github/workflows/containers/github-action-ci/Dockerfile index efe08ebc221c5..df390716adc63 100644 --- a/.github/workflows/containers/github-action-ci/Dockerfile +++ b/.github/workflows/containers/github-action-ci/Dockerfile @@ -58,16 +58,24 @@ RUN apt-get update && \ python3-psutil \ sudo \ # These are needed by the premerge pipeline. Pip is used to install - # dependent python packages and ccache is used for build caching. File and - # tzdata are used for tests. + # dependent python packages. File and tzdata are used for tests. python3-pip \ - ccache \ file \ - tzdata \ - sccache && \ + tzdata && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* +# We need sccache for caching. We cannot use the apt repository version because +# it is too old and has bugs related to features we require (particularly GCS +# caching), so we manually install it here. +# TODO(boomanaiden154): We should return to installing this from the apt +# repository once a version containing the necessary bug fixes is available. +RUN curl -L 'https://github.com/mozilla/sccache/releases/download/v0.10.0/sccache-v0.10.0-x86_64-unknown-linux-musl.tar.gz' > /tmp/sccache.tar.gz && \ + echo "1fbb35e135660d04a2d5e42b59c7874d39b3deb17de56330b25b713ec59f849b /tmp/sccache.tar.gz" | sha256sum -c && \ + tar xzf /tmp/sccache.tar.gz -O --wildcards '*/sccache' > '/usr/local/bin/sccache' && \ + rm /tmp/sccache.tar.gz && \ + chmod +x /usr/local/bin/sccache + ENV LLVM_SYSROOT=$LLVM_SYSROOT ENV PATH=${LLVM_SYSROOT}/bin:${PATH} @@ -86,7 +94,7 @@ WORKDIR /home/gha FROM ci-container as ci-container-agent -ENV GITHUB_RUNNER_VERSION=2.326.0 +ENV GITHUB_RUNNER_VERSION=2.327.0 RUN mkdir actions-runner && \ cd actions-runner && \ diff --git a/.github/workflows/libcxx-build-containers.yml b/.github/workflows/libcxx-build-containers.yml index 564a79341edb1..f432e3ddd5d1e 100644 --- a/.github/workflows/libcxx-build-containers.yml +++ b/.github/workflows/libcxx-build-containers.yml @@ -18,8 +18,6 @@ on: - 'libcxx/utils/ci/**' - '.github/workflows/libcxx-build-containers.yml' pull_request: - branches: - - main paths: - 'libcxx/utils/ci/**' - '.github/workflows/libcxx-build-containers.yml' diff --git a/.github/workflows/premerge.yaml b/.github/workflows/premerge.yaml index f7a48304b82b0..d0518fa6879e2 100644 --- a/.github/workflows/premerge.yaml +++ b/.github/workflows/premerge.yaml @@ -34,10 +34,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - - name: Setup ccache - uses: hendrikmuhs/ccache-action@a1209f81afb8c005c13b4296c32e363431bffea5 # v1.2.17 - with: - max-size: "2000M" - name: Build and Test # Mark the job as a success even if the step fails so that people do # not get notified while the new premerge pipeline is in an @@ -61,7 +57,20 @@ jobs: export CC=/opt/llvm/bin/clang export CXX=/opt/llvm/bin/clang++ - ./.ci/monolithic-linux.sh "${projects_to_build}" "${project_check_targets}" "${runtimes_to_build}" "${runtimes_check_targets}" "${runtimes_check_targets_needs_reconfig}" + # This environment variable is passes into the container through the + # runner pod definition. This differs between our two clusters which + # why we do not hardcode it. + export SCCACHE_GCS_BUCKET=$CACHE_GCS_BUCKET + export SCCACHE_GCS_RW_MODE=READ_WRITE + + # Set the idle timeout to zero to ensure sccache runs for the + # entire duration of the job. Otherwise it might stop if we run + # several test suites in a row and discard statistics that we want + # to save in the end. + export SCCACHE_IDLE_TIMEOUT=0 + sccache --start-server + + ./.ci/monolithic-linux.sh "${projects_to_build}" "${project_check_targets}" "${runtimes_to_build}" "${runtimes_check_targets}" "${runtimes_check_targets_needs_reconfig}" "${enable_cir}" - name: Upload Artifacts if: '!cancelled()' uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 @@ -76,7 +85,7 @@ jobs: if: >- github.repository_owner == 'llvm' && (github.event_name != 'pull_request' || github.event.action != 'closed') - runs-on: llvm-premerge-windows-runners + runs-on: llvm-premerge-windows-2022-runners defaults: run: shell: bash @@ -85,11 +94,6 @@ jobs: uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 2 - - name: Setup ccache - uses: hendrikmuhs/ccache-action@a1209f81afb8c005c13b4296c32e363431bffea5 # v1.2.17 - with: - variant: "sccache" - max-size: "2000M" - name: Compute Projects id: vars run: | @@ -112,7 +116,9 @@ jobs: shell: cmd run: | call C:\\BuildTools\\Common7\\Tools\\VsDevCmd.bat -arch=amd64 -host_arch=amd64 - bash .ci/monolithic-windows.sh "${{ steps.vars.outputs.windows-projects }}" "${{ steps.vars.outputs.windows-check-targets }}" + # See the comments above in the Linux job for why we define each of + # these environment variables. + bash -c "export SCCACHE_GCS_BUCKET=$CACHE_GCS_BUCKET; export SCCACHE_GCS_RW_MODE=READ_WRITE; export SCCACHE_IDLE_TIMEOUT=0; sccache --start-server; .ci/monolithic-windows.sh \"${{ steps.vars.outputs.windows-projects }}\" \"${{ steps.vars.outputs.windows-check-targets }}\"" - name: Upload Artifacts if: '!cancelled()' uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 diff --git a/.github/workflows/release-asset-audit.yml b/.github/workflows/release-asset-audit.yml index 8112d8a140810..7a1f232ae7335 100644 --- a/.github/workflows/release-asset-audit.yml +++ b/.github/workflows/release-asset-audit.yml @@ -22,7 +22,12 @@ jobs: runs-on: ubuntu-24.04 if: github.repository == 'llvm/llvm-project' steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 #v4.1.6 + - name: Checkout LLVM + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: | + .github/workflows/release-asset-audit.py + llvm/utils/git/requirements.txt - name: "Run Audit Script" env: GITHUB_TOKEN: ${{ github.token }} diff --git a/bolt/lib/Profile/DataAggregator.cpp b/bolt/lib/Profile/DataAggregator.cpp index 905728de8262f..3604fdd3a94b4 100644 --- a/bolt/lib/Profile/DataAggregator.cpp +++ b/bolt/lib/Profile/DataAggregator.cpp @@ -906,11 +906,10 @@ DataAggregator::getFallthroughsInTrace(BinaryFunction &BF, const Trace &Trace, if (BF.isPseudo()) return Branches; - if (!BF.isSimple()) + // Can only record traces in CFG state + if (!BF.hasCFG()) return std::nullopt; - assert(BF.hasCFG() && "can only record traces in CFG state"); - const BinaryBasicBlock *FromBB = BF.getBasicBlockContainingOffset(From); const BinaryBasicBlock *ToBB = BF.getBasicBlockContainingOffset(To); diff --git a/bolt/lib/Rewrite/RewriteInstance.cpp b/bolt/lib/Rewrite/RewriteInstance.cpp index 96045a916232c..9f243a1366928 100644 --- a/bolt/lib/Rewrite/RewriteInstance.cpp +++ b/bolt/lib/Rewrite/RewriteInstance.cpp @@ -714,21 +714,6 @@ Error RewriteInstance::run() { preprocessProfileData(); - // Skip disassembling if we have a translation table and we are running an - // aggregation job. - if (opts::AggregateOnly && BAT->enabledFor(InputFile)) { - // YAML profile in BAT mode requires CFG for .bolt.org.text functions - if (!opts::SaveProfile.empty() || - opts::ProfileFormat == opts::ProfileFormatKind::PF_YAML) { - selectFunctionsToProcess(); - disassembleFunctions(); - processMetadataPreCFG(); - buildFunctionsCFG(); - } - processProfileData(); - return Error::success(); - } - selectFunctionsToProcess(); readDebugInfo(); @@ -4260,31 +4245,25 @@ void RewriteInstance::patchELFPHDRTable() { const ELFFile &Obj = ELF64LEFile->getELFFile(); raw_fd_ostream &OS = Out->os(); - // Write/re-write program headers. Phnum = Obj.getHeader().e_phnum; - if (PHDRTableOffset) { - // Writing new pheader table and adding one new entry for R+X segment. - Phnum += 1; - if (NewWritableSegmentSize) { - // Adding one more entry for R+W segment. - Phnum += 1; - } - } else { + + if (BC->NewSegments.empty()) { + BC->outs() << "BOLT-INFO: not adding new segments\n"; + return; + } + + if (opts::UseGnuStack) { assert(!PHDRTableAddress && "unexpected address for program header table"); - PHDRTableOffset = Obj.getHeader().e_phoff; - if (NewWritableSegmentSize) { + if (BC->NewSegments.size() > 1) { BC->errs() << "BOLT-ERROR: unable to add writable segment\n"; exit(1); } + } else { + Phnum += BC->NewSegments.size(); } - if (opts::Instrument) - Phnum += 2; - - if (BC->NewSegments.empty()) { - BC->outs() << "BOLT-INFO: not adding new segments\n"; - return; - } + if (!PHDRTableOffset) + PHDRTableOffset = Obj.getHeader().e_phoff; const uint64_t SavedPos = OS.tell(); OS.seek(PHDRTableOffset); diff --git a/bolt/test/X86/debug-fission-single-convert.s b/bolt/test/X86/debug-fission-single-convert.s index 02c9290211fc0..ea05ccde118ef 100644 --- a/bolt/test/X86/debug-fission-single-convert.s +++ b/bolt/test/X86/debug-fission-single-convert.s @@ -14,14 +14,15 @@ # RUN: -nostartfiles \ # RUN: -Wl,--script=%p/Inputs/debug-fission-script.txt \ # RUN: %t.o -o %t.exe +# RUN: mkdir -p %t.dwarf-output # RUN: llvm-bolt %t.exe \ # RUN: --reorder-blocks=reverse \ # RUN: --update-debug-sections \ -# RUN: --dwarf-output-path=%T \ +# RUN: --dwarf-output-path=%t.dwarf-output \ # RUN: --always-convert-to-ranges=true \ # RUN: -o %t.bolt.1.exe 2>&1 | FileCheck %s # RUN: llvm-dwarfdump --show-form --verbose --debug-ranges %t.bolt.1.exe &> %tAddrIndexTest -# RUN: not llvm-dwarfdump --show-form --verbose --debug-info %T/debug-fission-simple-convert.dwo0.dwo >> %tAddrIndexTest +# RUN: not llvm-dwarfdump --show-form --verbose --debug-info %t.dwarf-output/debug-fission-simple-convert.dwo0.dwo >> %tAddrIndexTest # RUN: cat %tAddrIndexTest | FileCheck %s --check-prefix=CHECK-DWO-DWO # RUN: llvm-dwarfdump --show-form --verbose --debug-addr %t.bolt.1.exe | FileCheck %s --check-prefix=CHECK-ADDR-SEC diff --git a/bolt/test/X86/debug-fission-single.s b/bolt/test/X86/debug-fission-single.s index 1aa502fc9a840..7ff53df9d412c 100644 --- a/bolt/test/X86/debug-fission-single.s +++ b/bolt/test/X86/debug-fission-single.s @@ -14,13 +14,14 @@ # RUN: -nostartfiles \ # RUN: -Wl,--script=%p/Inputs/debug-fission-script.txt \ # RUN: %t.o -o %t.exe +# RUN: mkdir -p %t.dwarf-output # RUN: llvm-bolt %t.exe \ # RUN: --reorder-blocks=reverse \ # RUN: --update-debug-sections \ -# RUN: --dwarf-output-path=%T \ +# RUN: --dwarf-output-path=%t.dwarf-output \ # RUN: -o %t.bolt.1.exe 2>&1 | FileCheck %s # RUN: llvm-dwarfdump --show-form --verbose --debug-ranges %t.bolt.1.exe &> %tAddrIndexTest -# RUN: llvm-dwarfdump --show-form --verbose --debug-info %T/debug-fission-simple.dwo0.dwo >> %tAddrIndexTest +# RUN: llvm-dwarfdump --show-form --verbose --debug-info %t.dwarf-output/debug-fission-simple.dwo0.dwo >> %tAddrIndexTest # RUN: cat %tAddrIndexTest | FileCheck %s --check-prefix=CHECK-DWO-DWO # RUN: llvm-dwarfdump --show-form --verbose --debug-addr %t.bolt.1.exe | FileCheck %s --check-prefix=CHECK-ADDR-SEC diff --git a/bolt/test/X86/inlined-function-mixed.test b/bolt/test/X86/inlined-function-mixed.test index 9f6ef396bb159..4fc1594f81729 100644 --- a/bolt/test/X86/inlined-function-mixed.test +++ b/bolt/test/X86/inlined-function-mixed.test @@ -1,9 +1,9 @@ ## Make sure inlining from a unit with debug info into unit without ## debug info does not cause a crash. -RUN: %clangxx %cxxflags %S/Inputs/inlined.cpp -c -o %T/inlined.o -RUN: %clangxx %cxxflags %S/Inputs/inlinee.cpp -c -o %T/inlinee.o -g -RUN: %clangxx %cxxflags %T/inlined.o %T/inlinee.o -o %t +RUN: %clangxx %cxxflags %S/Inputs/inlined.cpp -c -o %t.inlined.o +RUN: %clangxx %cxxflags %S/Inputs/inlinee.cpp -c -o %t.inlinee.o -g +RUN: %clangxx %cxxflags %t.inlined.o %t.inlinee.o -o %t RUN: llvm-bolt %t -o %t.bolt --update-debug-sections --reorder-blocks=reverse \ RUN: --inline-small-functions --force-inline=main | FileCheck %s diff --git a/bolt/test/X86/unclaimed-jt-entries.s b/bolt/test/X86/unclaimed-jt-entries.s index 1102e4ae413e2..31b72c47125ae 100644 --- a/bolt/test/X86/unclaimed-jt-entries.s +++ b/bolt/test/X86/unclaimed-jt-entries.s @@ -18,6 +18,18 @@ # RUN: llvm-mc -filetype=obj -triple x86_64-unknown-unknown %s -o %t.o # RUN: %clang %cflags -no-pie %t.o -o %t.exe -Wl,-q + +## Check that non-simple function profile is emitted in perf2bolt mode +# RUN: link_fdata %s %t.exe %t.pa PREAGG +# RUN: llvm-strip -N L5 -N L5_ret %t.exe +# RUN: perf2bolt %t.exe -p %t.pa --pa -o %t.fdata -strict=0 -print-profile \ +# RUN: -print-only=main | FileCheck %s --check-prefix=CHECK-P2B +# CHECK-P2B: PERF2BOLT: traces mismatching disassembled function contents: 0 +# CHECK-P2B: Binary Function "main" +# CHECK-P2B: IsSimple : 0 +# RUN: FileCheck %s --input-file %t.fdata --check-prefix=CHECK-FDATA +# CHECK-FDATA: 1 main 0 1 main 7 0 1 + # RUN: llvm-bolt %t.exe -v=1 -o %t.out 2>&1 | FileCheck %s # CHECK: BOLT-WARNING: unclaimed data to code reference (possibly an unrecognized jump table entry) to .Ltmp[[#]] in main @@ -33,8 +45,10 @@ .size main, .Lend-main main: jmp *L4-24(,%rdi,8) -.L5: +# PREAGG: T #main# #L5# #L5_ret# 1 +L5: movl $4, %eax +L5_ret: ret .L9: movl $2, %eax @@ -58,7 +72,7 @@ L4: .quad .L3 .quad .L6 .quad .L3 - .quad .L5 + .quad L5 .quad .L3 .quad .L3 .quad .L3 diff --git a/bolt/utils/nfc-check-setup.py b/bolt/utils/nfc-check-setup.py index 275ac7b886d00..d8666e2158499 100755 --- a/bolt/utils/nfc-check-setup.py +++ b/bolt/utils/nfc-check-setup.py @@ -7,6 +7,8 @@ import sys import textwrap +msg_prefix = "\n> NFC-Mode:" + def get_relevant_bolt_changes(dir: str) -> str: # Return a list of bolt source changes that are relevant to testing. all_changes = subprocess.run( @@ -42,14 +44,32 @@ def get_git_ref_or_rev(dir: str) -> str: cmd_rev = "git rev-parse --short HEAD" return subprocess.check_output(shlex.split(cmd_rev), cwd=dir, text=True).strip() +def switch_back( + switch_back: bool, stash: bool, source_dir: str, old_ref: str, new_ref: str +): + # Switch back to the current revision if needed and inform the user of where + # the HEAD is. Must be called after checking out the previous commit on all + # exit paths. + if switch_back: + print(f"{msg_prefix} Switching back to current revision..") + if stash: + subprocess.run(shlex.split("git stash pop"), cwd=source_dir) + subprocess.run(shlex.split(f"git checkout {old_ref}"), cwd=source_dir) + else: + print( + f"The repository {source_dir} has been switched from {old_ref} " + f"to {new_ref}. Local changes were stashed. Switch back using\n\t" + f"git checkout {old_ref}\n" + ) def main(): parser = argparse.ArgumentParser( description=textwrap.dedent( """ - This script builds two versions of BOLT (with the current and - previous revision) and sets up symlink for llvm-bolt-wrapper. - Passes the options through to llvm-bolt-wrapper. + This script builds two versions of BOLT: + llvm-bolt.new, using the current revision, and llvm-bolt.old using + the previous revision. These can be used to check whether the + current revision changes BOLT's functional behavior. """ ) ) @@ -59,6 +79,12 @@ def main(): default=os.getcwd(), help="Path to BOLT build directory, default is current " "directory", ) + parser.add_argument( + "--create-wrapper", + default=False, + action="store_true", + help="Sets up llvm-bolt as a symlink to llvm-bolt-wrapper. Passes the options through to llvm-bolt-wrapper.", + ) parser.add_argument( "--check-bolt-sources", default=False, @@ -76,28 +102,42 @@ def main(): default="HEAD^", help="Revision to checkout to compare vs HEAD", ) + + # When creating a wrapper, pass any unknown arguments to it. Otherwise, die. args, wrapper_args = parser.parse_known_args() - bolt_path = f"{args.build_dir}/bin/llvm-bolt" + if not args.create_wrapper and len(wrapper_args) > 0: + parser.parse_args() + # Find the repo directory. source_dir = None - # find the repo directory - with open(f"{args.build_dir}/CMakeCache.txt") as f: - for line in f: - m = re.match(r"LLVM_SOURCE_DIR:STATIC=(.*)", line) - if m: - source_dir = m.groups()[0] - if not source_dir: - sys.exit("Source directory is not found") - - script_dir = os.path.dirname(os.path.abspath(__file__)) - wrapper_path = f"{script_dir}/llvm-bolt-wrapper.py" - # build the current commit + try: + CMCacheFilename = f"{args.build_dir}/CMakeCache.txt" + with open(CMCacheFilename) as f: + for line in f: + m = re.match(r"LLVM_SOURCE_DIR:STATIC=(.*)", line) + if m: + source_dir = m.groups()[0] + if not source_dir: + raise Exception(f"Source directory not found: '{CMCacheFilename}'") + except Exception as e: + sys.exit(e) + + # Clean the previous llvm-bolt if it exists. + bolt_path = f"{args.build_dir}/bin/llvm-bolt" + if os.path.exists(bolt_path): + os.remove(bolt_path) + + # Build the current commit. + print(f"{msg_prefix} Building current revision..") subprocess.run( shlex.split("cmake --build . --target llvm-bolt"), cwd=args.build_dir ) - # rename llvm-bolt + + if not os.path.exists(bolt_path): + sys.exit(f"Failed to build the current revision: '{bolt_path}'") + + # Rename llvm-bolt and memorize the old hash for logging. os.replace(bolt_path, f"{bolt_path}.new") - # memorize the old hash for logging old_ref = get_git_ref_or_rev(source_dir) if args.check_bolt_sources: @@ -110,7 +150,7 @@ def main(): print(f"BOLT source changes were found:\n{file_changes}") open(marker, "a").close() - # determine whether a stash is needed + # Determine whether a stash is needed. stash = subprocess.run( shlex.split("git status --porcelain"), cwd=source_dir, @@ -119,42 +159,59 @@ def main(): text=True, ).stdout if stash: - # save local changes before checkout + # Save local changes before checkout. subprocess.run(shlex.split("git stash push -u"), cwd=source_dir) - # check out the previous/cmp commit + + # Check out the previous/cmp commit and get its commit hash for logging. subprocess.run(shlex.split(f"git checkout -f {args.cmp_rev}"), cwd=source_dir) - # get the parent commit hash for logging new_ref = get_git_ref_or_rev(source_dir) - # build the previous commit + + # Build the previous commit. + print(f"{msg_prefix} Building previous revision..") subprocess.run( shlex.split("cmake --build . --target llvm-bolt"), cwd=args.build_dir ) - # rename llvm-bolt + + # Rename llvm-bolt. + if not os.path.exists(bolt_path): + print(f"Failed to build the previous revision: '{bolt_path}'") + switch_back(args.switch_back, stash, source_dir, old_ref, new_ref) + sys.exit(1) os.replace(bolt_path, f"{bolt_path}.old") - # set up llvm-bolt-wrapper.ini - ini = subprocess.check_output( - shlex.split(f"{wrapper_path} {bolt_path}.old {bolt_path}.new") + wrapper_args, - text=True, + + # Symlink llvm-bolt-wrapper + if args.create_wrapper: + print(f"{msg_prefix} Creating llvm-bolt wrapper..") + script_dir = os.path.dirname(os.path.abspath(__file__)) + wrapper_path = f"{script_dir}/llvm-bolt-wrapper.py" + try: + # Set up llvm-bolt-wrapper.ini + ini = subprocess.check_output( + shlex.split(f"{wrapper_path} {bolt_path}.old {bolt_path}.new") + + wrapper_args, + text=True, + ) + with open(f"{args.build_dir}/bin/llvm-bolt-wrapper.ini", "w") as f: + f.write(ini) + os.symlink(wrapper_path, bolt_path) + except Exception as e: + print("Failed to create a wrapper:\n" + str(e)) + switch_back(args.switch_back, stash, source_dir, old_ref, new_ref) + sys.exit(1) + + switch_back(args.switch_back, stash, source_dir, old_ref, new_ref) + + print( + f"{msg_prefix} Completed!\nBuild directory {args.build_dir} is ready for" + " NFC-Mode comparison between the two revisions." ) - with open(f"{args.build_dir}/bin/llvm-bolt-wrapper.ini", "w") as f: - f.write(ini) - # symlink llvm-bolt-wrapper - os.symlink(wrapper_path, bolt_path) - if args.switch_back: - if stash: - subprocess.run(shlex.split("git stash pop"), cwd=source_dir) - subprocess.run(shlex.split(f"git checkout {old_ref}"), cwd=source_dir) - else: + + if args.create_wrapper: print( - f"The repository {source_dir} has been switched from {old_ref} " - f"to {new_ref}. Local changes were stashed. Switch back using\n\t" - f"git checkout {old_ref}\n" + "Can run BOLT tests using:\n" + "\tbin/llvm-lit -sv tools/bolt/test\nor\n" + "\tbin/llvm-lit -sv tools/bolttests" ) - print( - f"Build directory {args.build_dir} is ready to run BOLT tests, e.g.\n" - "\tbin/llvm-lit -sv tools/bolt/test\nor\n" - "\tbin/llvm-lit -sv tools/bolttests" - ) if __name__ == "__main__": diff --git a/clang-tools-extra/README.txt b/clang-tools-extra/README.txt index 6891e4078997f..1195db9b468dd 100644 --- a/clang-tools-extra/README.txt +++ b/clang-tools-extra/README.txt @@ -8,12 +8,13 @@ Clang frontend. These tools are kept in a separate "extra" repository to allow lighter weight checkouts of the core Clang codebase. All discussion regarding Clang, Clang-based tools, and code in this repository -should be held using the standard Clang forum: +should be held using the standard Clang forums: https://discourse.llvm.org/c/clang + https://discourse.llvm.org/c/clang/clang-tidy/71 + https://discourse.llvm.org/c/clang/clangd/34 -Code review for this tree should take place on the standard Clang patch and -commit lists: - http://lists.llvm.org/mailman/listinfo/cfe-commits +Code review for this tree should take place on Github: + https://github.com/llvm/llvm-project/pulls?q=label%3Aclang-tools-extra If you find a bug in these tools, please file it in the LLVM bug tracker: https://github.com/llvm/llvm-project/issues/ diff --git a/clang-tools-extra/clang-doc/BitcodeReader.cpp b/clang-tools-extra/clang-doc/BitcodeReader.cpp index dce34a8434ff8..4efbbd34730cf 100644 --- a/clang-tools-extra/clang-doc/BitcodeReader.cpp +++ b/clang-tools-extra/clang-doc/BitcodeReader.cpp @@ -384,6 +384,8 @@ static llvm::Error parseRecord(const Record &R, unsigned ID, return decodeRecord(R, I->Path, Blob); case REFERENCE_FIELD: return decodeRecord(R, F, Blob); + case REFERENCE_FILE: + return decodeRecord(R, I->DocumentationFileName, Blob); default: return llvm::createStringError(llvm::inconvertibleErrorCode(), "invalid field for Reference"); diff --git a/clang-tools-extra/clang-doc/BitcodeWriter.cpp b/clang-tools-extra/clang-doc/BitcodeWriter.cpp index eed23726e17bf..e23511bf63690 100644 --- a/clang-tools-extra/clang-doc/BitcodeWriter.cpp +++ b/clang-tools-extra/clang-doc/BitcodeWriter.cpp @@ -210,6 +210,7 @@ static const llvm::IndexedMap {REFERENCE_TYPE, {"RefType", &genIntAbbrev}}, {REFERENCE_PATH, {"Path", &genStringAbbrev}}, {REFERENCE_FIELD, {"Field", &genIntAbbrev}}, + {REFERENCE_FILE, {"File", &genStringAbbrev}}, {TEMPLATE_PARAM_CONTENTS, {"Contents", &genStringAbbrev}}, {TEMPLATE_SPECIALIZATION_OF, {"SpecializationOf", &genSymbolIdAbbrev}}, @@ -286,7 +287,7 @@ static const std::vector>> // Reference Block {BI_REFERENCE_BLOCK_ID, {REFERENCE_USR, REFERENCE_NAME, REFERENCE_QUAL_NAME, REFERENCE_TYPE, - REFERENCE_PATH, REFERENCE_FIELD}}, + REFERENCE_PATH, REFERENCE_FIELD, REFERENCE_FILE}}, // Template Blocks. {BI_TEMPLATE_BLOCK_ID, {}}, {BI_TEMPLATE_PARAM_BLOCK_ID, {TEMPLATE_PARAM_CONTENTS}}, @@ -479,6 +480,7 @@ void ClangDocBitcodeWriter::emitBlock(const Reference &R, FieldId Field) { emitRecord((unsigned)R.RefType, REFERENCE_TYPE); emitRecord(R.Path, REFERENCE_PATH); emitRecord((unsigned)Field, REFERENCE_FIELD); + emitRecord(R.DocumentationFileName, REFERENCE_FILE); } void ClangDocBitcodeWriter::emitBlock(const FriendInfo &R) { diff --git a/clang-tools-extra/clang-doc/BitcodeWriter.h b/clang-tools-extra/clang-doc/BitcodeWriter.h index 501af12582a8e..688f886b45308 100644 --- a/clang-tools-extra/clang-doc/BitcodeWriter.h +++ b/clang-tools-extra/clang-doc/BitcodeWriter.h @@ -140,6 +140,7 @@ enum RecordId { REFERENCE_TYPE, REFERENCE_PATH, REFERENCE_FIELD, + REFERENCE_FILE, TEMPLATE_PARAM_CONTENTS, TEMPLATE_SPECIALIZATION_OF, TYPEDEF_USR, diff --git a/clang-tools-extra/clang-doc/HTMLMustacheGenerator.cpp b/clang-tools-extra/clang-doc/HTMLMustacheGenerator.cpp index 7aeaa1b7cf67d..a64cb5ea26a79 100644 --- a/clang-tools-extra/clang-doc/HTMLMustacheGenerator.cpp +++ b/clang-tools-extra/clang-doc/HTMLMustacheGenerator.cpp @@ -27,6 +27,9 @@ using namespace llvm::mustache; namespace clang { namespace doc { +static Error generateDocForJSON(json::Value &JSON, StringRef Filename, + StringRef Path, raw_fd_ostream &OS, + const ClangDocContext &CDCtx); static Error createFileOpenError(StringRef FileName, std::error_code EC) { return createFileError("cannot open file " + FileName, EC); @@ -132,404 +135,68 @@ Error MustacheHTMLGenerator::generateDocs( return Err; } - // Track which directories we already tried to create. - StringSet<> CreatedDirs; - // Collect all output by file name and create the necessary directories. - StringMap> FileToInfos; - for (const auto &Group : Infos) { - llvm::TimeTraceScope TS("setup directories"); - doc::Info *Info = Group.getValue().get(); - - SmallString<128> Path; - sys::path::native(RootDir, Path); - sys::path::append(Path, Info->getRelativeFilePath("")); - if (!CreatedDirs.contains(Path)) { - if (std::error_code EC = sys::fs::create_directories(Path)) - return createStringError(EC, "failed to create directory '%s'.", - Path.c_str()); - CreatedDirs.insert(Path); - } - - sys::path::append(Path, Info->getFileBaseName() + ".html"); - FileToInfos[Path].push_back(Info); + { + llvm::TimeTraceScope TS("Generate JSON for Mustache"); + if (auto JSONGenerator = findGeneratorByName("json")) { + if (Error Err = JSONGenerator.get()->generateDocs( + RootDir, std::move(Infos), CDCtx)) + return Err; + } else + return JSONGenerator.takeError(); } + StringMap JSONFileMap; { - llvm::TimeTraceScope TS("Generate Docs"); - for (const auto &Group : FileToInfos) { - llvm::TimeTraceScope TS("Info to Doc"); + llvm::TimeTraceScope TS("Iterate JSON files"); + std::error_code EC; + sys::fs::directory_iterator JSONIter(RootDir, EC); + std::vector JSONFiles; + JSONFiles.reserve(Infos.size()); + if (EC) + return createStringError("Failed to create directory iterator."); + + while (JSONIter != sys::fs::directory_iterator()) { + if (EC) + return createFileError("Failed to iterate: " + JSONIter->path(), EC); + + auto Path = StringRef(JSONIter->path()); + if (!Path.ends_with(".json")) { + JSONIter.increment(EC); + continue; + } + + auto File = MemoryBuffer::getFile(Path); + if (EC = File.getError(); EC) + // TODO: Buffer errors to report later, look into using Clang + // diagnostics. + llvm::errs() << "Failed to open file: " << Path << " " << EC.message() + << '\n'; + + auto Parsed = json::parse((*File)->getBuffer()); + if (!Parsed) + return Parsed.takeError(); + std::error_code FileErr; - raw_fd_ostream InfoOS(Group.getKey(), FileErr, sys::fs::OF_None); + SmallString<16> HTMLPath(Path.begin(), Path.end()); + sys::path::replace_extension(HTMLPath, "html"); + raw_fd_ostream InfoOS(HTMLPath, FileErr, sys::fs::OF_None); if (FileErr) - return createFileOpenError(Group.getKey(), FileErr); + return createFileOpenError(Path, FileErr); - for (const auto &Info : Group.getValue()) - if (Error Err = generateDocForInfo(Info, InfoOS, CDCtx)) - return Err; + if (Error Err = generateDocForJSON(*Parsed, sys::path::stem(HTMLPath), + HTMLPath, InfoOS, CDCtx)) + return Err; + JSONIter.increment(EC); } } - return Error::success(); -} - -static json::Value -extractValue(const Location &L, - std::optional RepositoryUrl = std::nullopt) { - Object Obj = Object(); - // TODO: Consider using both Start/End line numbers to improve location report - Obj.insert({"LineNumber", L.StartLineNumber}); - Obj.insert({"Filename", L.Filename}); - - if (!L.IsFileInRootDir || !RepositoryUrl) - return Obj; - SmallString<128> FileURL(*RepositoryUrl); - sys::path::append(FileURL, sys::path::Style::posix, L.Filename); - FileURL += "#" + std::to_string(L.StartLineNumber); - Obj.insert({"FileURL", FileURL}); - - return Obj; -} - -static json::Value extractValue(const Reference &I, - StringRef CurrentDirectory) { - SmallString<64> Path = I.getRelativeFilePath(CurrentDirectory); - sys::path::append(Path, I.getFileBaseName() + ".html"); - sys::path::native(Path, sys::path::Style::posix); - Object Obj = Object(); - Obj.insert({"Link", Path}); - Obj.insert({"Name", I.Name}); - Obj.insert({"QualName", I.QualName}); - Obj.insert({"ID", toHex(toStringRef(I.USR))}); - return Obj; -} - -static json::Value extractValue(const TypedefInfo &I) { - // Not Supported - return nullptr; -} - -static json::Value extractValue(const CommentInfo &I) { - Object Obj = Object(); - - json::Value ChildVal = Object(); - Object &Child = *ChildVal.getAsObject(); - - json::Value ChildArr = Array(); - auto &CARef = *ChildArr.getAsArray(); - CARef.reserve(I.Children.size()); - for (const auto &C : I.Children) - CARef.emplace_back(extractValue(*C)); - - switch (I.Kind) { - case CommentKind::CK_TextComment: { - Obj.insert({commentKindToString(I.Kind), I.Text}); - return Obj; - } - - case CommentKind::CK_BlockCommandComment: { - Child.insert({"Command", I.Name}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_InlineCommandComment: { - json::Value ArgsArr = Array(); - auto &ARef = *ArgsArr.getAsArray(); - ARef.reserve(I.Args.size()); - for (const auto &Arg : I.Args) - ARef.emplace_back(Arg); - Child.insert({"Command", I.Name}); - Child.insert({"Args", ArgsArr}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_ParamCommandComment: - case CommentKind::CK_TParamCommandComment: { - Child.insert({"ParamName", I.ParamName}); - Child.insert({"Direction", I.Direction}); - Child.insert({"Explicit", I.Explicit}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_VerbatimBlockComment: { - Child.insert({"Text", I.Text}); - if (!I.CloseName.empty()) - Child.insert({"CloseName", I.CloseName}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_VerbatimBlockLineComment: - case CommentKind::CK_VerbatimLineComment: { - Child.insert({"Text", I.Text}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_HTMLStartTagComment: { - json::Value AttrKeysArray = json::Array(); - json::Value AttrValuesArray = json::Array(); - auto &KeyArr = *AttrKeysArray.getAsArray(); - auto &ValArr = *AttrValuesArray.getAsArray(); - KeyArr.reserve(I.AttrKeys.size()); - ValArr.reserve(I.AttrValues.size()); - for (const auto &K : I.AttrKeys) - KeyArr.emplace_back(K); - for (const auto &V : I.AttrValues) - ValArr.emplace_back(V); - Child.insert({"Name", I.Name}); - Child.insert({"SelfClosing", I.SelfClosing}); - Child.insert({"AttrKeys", AttrKeysArray}); - Child.insert({"AttrValues", AttrValuesArray}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_HTMLEndTagComment: { - Child.insert({"Name", I.Name}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_FullComment: - case CommentKind::CK_ParagraphComment: { - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; - } - - case CommentKind::CK_Unknown: { - Obj.insert({commentKindToString(I.Kind), I.Text}); - return Obj; - } - } - llvm_unreachable("Unknown comment kind encountered."); -} - -static void maybeInsertLocation(std::optional Loc, - const ClangDocContext &CDCtx, Object &Obj) { - if (!Loc) - return; - Location L = *Loc; - Obj.insert({"Location", extractValue(L, CDCtx.RepositoryUrl)}); -} - -static void extractDescriptionFromInfo(ArrayRef Descriptions, - json::Object &EnumValObj) { - if (Descriptions.empty()) - return; - json::Value DescArr = Array(); - json::Array &DescARef = *DescArr.getAsArray(); - DescARef.reserve(Descriptions.size()); - for (const CommentInfo &Child : Descriptions) - DescARef.emplace_back(extractValue(Child)); - EnumValObj.insert({"EnumValueComments", DescArr}); -} - -static json::Value extractValue(const FunctionInfo &I, StringRef ParentInfoDir, - const ClangDocContext &CDCtx) { - Object Obj = Object(); - Obj.insert({"Name", I.Name}); - Obj.insert({"ID", toHex(toStringRef(I.USR))}); - Obj.insert({"Access", getAccessSpelling(I.Access).str()}); - Obj.insert({"ReturnType", extractValue(I.ReturnType.Type, ParentInfoDir)}); - - json::Value ParamArr = Array(); - json::Array &ParamARef = *ParamArr.getAsArray(); - ParamARef.reserve(I.Params.size()); - for (const auto Val : enumerate(I.Params)) { - json::Value V = Object(); - auto &VRef = *V.getAsObject(); - VRef.insert({"Name", Val.value().Name}); - VRef.insert({"Type", Val.value().Type.Name}); - VRef.insert({"End", Val.index() + 1 == I.Params.size()}); - ParamARef.emplace_back(V); - } - Obj.insert({"Params", ParamArr}); - maybeInsertLocation(I.DefLoc, CDCtx, Obj); - return Obj; -} - -static json::Value extractValue(const EnumInfo &I, - const ClangDocContext &CDCtx) { - Object Obj = Object(); - std::string EnumType = I.Scoped ? "enum class " : "enum "; - EnumType += I.Name; - bool HasComment = llvm::any_of( - I.Members, [](const EnumValueInfo &M) { return !M.Description.empty(); }); - Obj.insert({"EnumName", EnumType}); - Obj.insert({"HasComment", HasComment}); - Obj.insert({"ID", toHex(toStringRef(I.USR))}); - json::Value EnumArr = Array(); - json::Array &EnumARef = *EnumArr.getAsArray(); - EnumARef.reserve(I.Members.size()); - for (const EnumValueInfo &M : I.Members) { - json::Value EnumValue = Object(); - auto &EnumValObj = *EnumValue.getAsObject(); - EnumValObj.insert({"Name", M.Name}); - if (!M.ValueExpr.empty()) - EnumValObj.insert({"ValueExpr", M.ValueExpr}); - else - EnumValObj.insert({"Value", M.Value}); - - extractDescriptionFromInfo(M.Description, EnumValObj); - EnumARef.emplace_back(EnumValue); - } - Obj.insert({"EnumValues", EnumArr}); - - extractDescriptionFromInfo(I.Description, Obj); - maybeInsertLocation(I.DefLoc, CDCtx, Obj); - - return Obj; -} - -static void extractScopeChildren(const ScopeChildren &S, Object &Obj, - StringRef ParentInfoDir, - const ClangDocContext &CDCtx) { - json::Value NamespaceArr = Array(); - json::Array &NamespaceARef = *NamespaceArr.getAsArray(); - NamespaceARef.reserve(S.Namespaces.size()); - for (const Reference &Child : S.Namespaces) - NamespaceARef.emplace_back(extractValue(Child, ParentInfoDir)); - - if (!NamespaceARef.empty()) - Obj.insert({"Namespace", Object{{"Links", NamespaceArr}}}); - - json::Value RecordArr = Array(); - json::Array &RecordARef = *RecordArr.getAsArray(); - RecordARef.reserve(S.Records.size()); - for (const Reference &Child : S.Records) - RecordARef.emplace_back(extractValue(Child, ParentInfoDir)); - - if (!RecordARef.empty()) - Obj.insert({"Record", Object{{"Links", RecordArr}}}); - - json::Value FunctionArr = Array(); - json::Array &FunctionARef = *FunctionArr.getAsArray(); - FunctionARef.reserve(S.Functions.size()); - - json::Value PublicFunctionArr = Array(); - json::Array &PublicFunctionARef = *PublicFunctionArr.getAsArray(); - PublicFunctionARef.reserve(S.Functions.size()); - - json::Value ProtectedFunctionArr = Array(); - json::Array &ProtectedFunctionARef = *ProtectedFunctionArr.getAsArray(); - ProtectedFunctionARef.reserve(S.Functions.size()); - - for (const FunctionInfo &Child : S.Functions) { - json::Value F = extractValue(Child, ParentInfoDir, CDCtx); - AccessSpecifier Access = Child.Access; - if (Access == AccessSpecifier::AS_public) - PublicFunctionARef.emplace_back(F); - else if (Access == AccessSpecifier::AS_protected) - ProtectedFunctionARef.emplace_back(F); - else - FunctionARef.emplace_back(F); - } - - if (!FunctionARef.empty()) - Obj.insert({"Function", Object{{"Obj", FunctionArr}}}); - - if (!PublicFunctionARef.empty()) - Obj.insert({"PublicFunction", Object{{"Obj", PublicFunctionArr}}}); - - if (!ProtectedFunctionARef.empty()) - Obj.insert({"ProtectedFunction", Object{{"Obj", ProtectedFunctionArr}}}); - - json::Value EnumArr = Array(); - auto &EnumARef = *EnumArr.getAsArray(); - EnumARef.reserve(S.Enums.size()); - for (const EnumInfo &Child : S.Enums) - EnumARef.emplace_back(extractValue(Child, CDCtx)); - - if (!EnumARef.empty()) - Obj.insert({"Enums", Object{{"Obj", EnumArr}}}); - - json::Value TypedefArr = Array(); - auto &TypedefARef = *TypedefArr.getAsArray(); - TypedefARef.reserve(S.Typedefs.size()); - for (const TypedefInfo &Child : S.Typedefs) - TypedefARef.emplace_back(extractValue(Child)); - - if (!TypedefARef.empty()) - Obj.insert({"Typedefs", Object{{"Obj", TypedefArr}}}); -} - -static json::Value extractValue(const NamespaceInfo &I, - const ClangDocContext &CDCtx) { - Object NamespaceValue = Object(); - std::string InfoTitle = I.Name.empty() ? "Global Namespace" - : (Twine("namespace ") + I.Name).str(); - - SmallString<64> BasePath = I.getRelativeFilePath(""); - NamespaceValue.insert({"NamespaceTitle", InfoTitle}); - NamespaceValue.insert({"NamespacePath", BasePath}); - - extractDescriptionFromInfo(I.Description, NamespaceValue); - extractScopeChildren(I.Children, NamespaceValue, BasePath, CDCtx); - return NamespaceValue; -} - -static json::Value extractValue(const RecordInfo &I, - const ClangDocContext &CDCtx) { - Object RecordValue = Object(); - extractDescriptionFromInfo(I.Description, RecordValue); - RecordValue.insert({"Name", I.Name}); - RecordValue.insert({"FullName", I.FullName}); - RecordValue.insert({"RecordType", getTagType(I.TagType)}); - - maybeInsertLocation(I.DefLoc, CDCtx, RecordValue); - - SmallString<64> BasePath = I.getRelativeFilePath(""); - extractScopeChildren(I.Children, RecordValue, BasePath, CDCtx); - json::Value PublicMembers = Array(); - json::Array &PubMemberRef = *PublicMembers.getAsArray(); - PubMemberRef.reserve(I.Members.size()); - json::Value ProtectedMembers = Array(); - json::Array &ProtMemberRef = *ProtectedMembers.getAsArray(); - ProtMemberRef.reserve(I.Members.size()); - json::Value PrivateMembers = Array(); - json::Array &PrivMemberRef = *PrivateMembers.getAsArray(); - PrivMemberRef.reserve(I.Members.size()); - for (const MemberTypeInfo &Member : I.Members) { - json::Value MemberValue = Object(); - auto &MVRef = *MemberValue.getAsObject(); - MVRef.insert({"Name", Member.Name}); - MVRef.insert({"Type", Member.Type.Name}); - extractDescriptionFromInfo(Member.Description, MVRef); - - if (Member.Access == AccessSpecifier::AS_public) - PubMemberRef.emplace_back(MemberValue); - else if (Member.Access == AccessSpecifier::AS_protected) - ProtMemberRef.emplace_back(MemberValue); - else if (Member.Access == AccessSpecifier::AS_private) - ProtMemberRef.emplace_back(MemberValue); - } - if (!PubMemberRef.empty()) - RecordValue.insert({"PublicMembers", Object{{"Obj", PublicMembers}}}); - if (!ProtMemberRef.empty()) - RecordValue.insert({"ProtectedMembers", Object{{"Obj", ProtectedMembers}}}); - if (!PrivMemberRef.empty()) - RecordValue.insert({"PrivateMembers", Object{{"Obj", PrivateMembers}}}); - - return RecordValue; + return Error::success(); } -static Error setupTemplateValue(const ClangDocContext &CDCtx, json::Value &V, - Info *I) { +static Error setupTemplateValue(const ClangDocContext &CDCtx, json::Value &V) { V.getAsObject()->insert({"ProjectName", CDCtx.ProjectName}); json::Value StylesheetArr = Array(); - auto InfoPath = I->getRelativeFilePath(""); - SmallString<128> RelativePath = computeRelativePath("", InfoPath); + SmallString<128> RelativePath("./"); sys::path::native(RelativePath, sys::path::Style::posix); auto *SSA = StylesheetArr.getAsArray(); @@ -555,38 +222,42 @@ static Error setupTemplateValue(const ClangDocContext &CDCtx, json::Value &V, return Error::success(); } -Error MustacheHTMLGenerator::generateDocForInfo(Info *I, raw_ostream &OS, - const ClangDocContext &CDCtx) { - switch (I->IT) { - case InfoType::IT_namespace: { - json::Value V = - extractValue(*static_cast(I), CDCtx); - if (auto Err = setupTemplateValue(CDCtx, V, I)) +static Error generateDocForJSON(json::Value &JSON, StringRef Filename, + StringRef Path, raw_fd_ostream &OS, + const ClangDocContext &CDCtx) { + auto StrValue = (*JSON.getAsObject())["InfoType"]; + if (StrValue.kind() != json::Value::Kind::String) + return createStringError("JSON file '%s' does not contain key: 'InfoType'.", + Filename.str().c_str()); + auto ObjTypeStr = StrValue.getAsString(); + if (!ObjTypeStr.has_value()) + return createStringError( + "JSON file '%s' does not contain 'InfoType' field as a string.", + Filename.str().c_str()); + + if (ObjTypeStr.value() == "namespace") { + if (auto Err = setupTemplateValue(CDCtx, JSON)) return Err; assert(NamespaceTemplate && "NamespaceTemplate is nullptr."); - NamespaceTemplate->render(V, OS); - break; - } - case InfoType::IT_record: { - json::Value V = - extractValue(*static_cast(I), CDCtx); - if (auto Err = setupTemplateValue(CDCtx, V, I)) + NamespaceTemplate->render(JSON, OS); + } else if (ObjTypeStr.value() == "record") { + if (auto Err = setupTemplateValue(CDCtx, JSON)) return Err; - // Serialize the JSON value to the output stream in a readable format. - RecordTemplate->render(V, OS); - break; + assert(RecordTemplate && "RecordTemplate is nullptr."); + RecordTemplate->render(JSON, OS); } + return Error::success(); +} + +Error MustacheHTMLGenerator::generateDocForInfo(Info *I, raw_ostream &OS, + const ClangDocContext &CDCtx) { + switch (I->IT) { case InfoType::IT_enum: - OS << "IT_enum\n"; - break; case InfoType::IT_function: - OS << "IT_Function\n"; - break; case InfoType::IT_typedef: - OS << "IT_typedef\n"; - break; + case InfoType::IT_namespace: + case InfoType::IT_record: case InfoType::IT_concept: - break; case InfoType::IT_variable: case InfoType::IT_friend: break; diff --git a/clang-tools-extra/clang-doc/JSONGenerator.cpp b/clang-tools-extra/clang-doc/JSONGenerator.cpp index 6fdc7196e9095..599b381cea60d 100644 --- a/clang-tools-extra/clang-doc/JSONGenerator.cpp +++ b/clang-tools-extra/clang-doc/JSONGenerator.cpp @@ -43,9 +43,33 @@ static auto SerializeReferenceLambda = [](const auto &Ref, Object &Object) { serializeReference(Ref, Object); }; +static std::string infoTypeToString(InfoType IT) { + switch (IT) { + case InfoType::IT_default: + return "default"; + case InfoType::IT_namespace: + return "namespace"; + case InfoType::IT_record: + return "record"; + case InfoType::IT_function: + return "function"; + case InfoType::IT_enum: + return "enum"; + case InfoType::IT_typedef: + return "typedef"; + case InfoType::IT_concept: + return "concept"; + case InfoType::IT_variable: + return "variable"; + case InfoType::IT_friend: + return "friend"; + } + llvm_unreachable("Unknown InfoType encountered."); +} + static json::Object serializeLocation(const Location &Loc, - const std::optional &RepositoryUrl) { + const std::optional RepositoryUrl) { Object LocationObj = Object(); LocationObj["LineNumber"] = Loc.StartLineNumber; LocationObj["Filename"] = Loc.Filename; @@ -59,7 +83,39 @@ serializeLocation(const Location &Loc, return LocationObj; } -static json::Value serializeComment(const CommentInfo &I) { +static void insertComment(Object &Description, json::Value &Comment, + StringRef Key) { + auto DescriptionIt = Description.find(Key); + + if (DescriptionIt == Description.end()) { + auto CommentsArray = json::Array(); + CommentsArray.push_back(Comment); + Description[Key] = std::move(CommentsArray); + Description["Has" + Key.str()] = true; + } else { + DescriptionIt->getSecond().getAsArray()->push_back(Comment); + } +} + +static json::Value extractTextComments(Object *ParagraphComment) { + if (!ParagraphComment) + return json::Object(); + return *ParagraphComment->get("Children"); +} + +static json::Value extractVerbatimComments(json::Array VerbatimLines) { + json::Value TextArray = json::Array(); + auto &TextArrayRef = *TextArray.getAsArray(); + for (auto &Line : VerbatimLines) + TextArrayRef.push_back(*Line.getAsObject() + ->get("VerbatimBlockLineComment") + ->getAsObject() + ->get("Text")); + + return TextArray; +} + +static Object serializeComment(const CommentInfo &I, Object &Description) { // taken from PR #142273 Object Obj = Object(); @@ -70,7 +126,7 @@ static json::Value serializeComment(const CommentInfo &I) { auto &CARef = *ChildArr.getAsArray(); CARef.reserve(I.Children.size()); for (const auto &C : I.Children) - CARef.emplace_back(serializeComment(*C)); + CARef.emplace_back(serializeComment(*C, Description)); switch (I.Kind) { case CommentKind::CK_TextComment: { @@ -79,9 +135,11 @@ static json::Value serializeComment(const CommentInfo &I) { } case CommentKind::CK_BlockCommandComment: { - Child.insert({"Command", I.Name}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); + auto TextCommentsArray = extractTextComments(CARef.front().getAsObject()); + if (I.Name == "brief") + insertComment(Description, TextCommentsArray, "BriefComments"); + else if (I.Name == "return") + insertComment(Description, TextCommentsArray, "ReturnComments"); return Obj; } @@ -103,17 +161,20 @@ static json::Value serializeComment(const CommentInfo &I) { Child.insert({"ParamName", I.ParamName}); Child.insert({"Direction", I.Direction}); Child.insert({"Explicit", I.Explicit}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); + auto TextCommentsArray = extractTextComments(CARef.front().getAsObject()); + Child.insert({"Children", TextCommentsArray}); + if (I.Kind == CommentKind::CK_ParamCommandComment) + insertComment(Description, ChildVal, "ParamComments"); return Obj; } case CommentKind::CK_VerbatimBlockComment: { - Child.insert({"Text", I.Text}); - if (!I.CloseName.empty()) - Child.insert({"CloseName", I.CloseName}); - Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); + if (I.CloseName == "endcode") { + // We don't support \code language specification + auto TextCommentsArray = extractVerbatimComments(CARef); + insertComment(Description, TextCommentsArray, "CodeComments"); + } else if (I.CloseName == "endverbatim") + insertComment(Description, ChildVal, "VerbatimComments"); return Obj; } @@ -155,8 +216,8 @@ static json::Value serializeComment(const CommentInfo &I) { case CommentKind::CK_FullComment: case CommentKind::CK_ParagraphComment: { Child.insert({"Children", ChildArr}); - Obj.insert({commentKindToString(I.Kind), ChildVal}); - return Obj; + Child["ParagraphComment"] = true; + return Child; } case CommentKind::CK_Unknown: { @@ -169,9 +230,12 @@ static json::Value serializeComment(const CommentInfo &I) { static void serializeCommonAttributes(const Info &I, json::Object &Obj, - const std::optional &RepositoryUrl) { + const std::optional RepositoryUrl) { Obj["Name"] = I.Name; Obj["USR"] = toHex(toStringRef(I.USR)); + Obj["InfoType"] = infoTypeToString(I.IT); + if (!I.DocumentationFileName.empty()) + Obj["DocumentationFileName"] = I.DocumentationFileName; if (!I.Path.empty()) Obj["Path"] = I.Path; @@ -183,12 +247,20 @@ serializeCommonAttributes(const Info &I, json::Object &Obj, } if (!I.Description.empty()) { - json::Value DescArray = json::Array(); - auto &DescArrayRef = *DescArray.getAsArray(); - DescArrayRef.reserve(I.Description.size()); - for (const auto &Comment : I.Description) - DescArrayRef.push_back(serializeComment(Comment)); - Obj["Description"] = DescArray; + Object Description = Object(); + // Skip straight to the FullComment's children + auto &Comments = I.Description.at(0).Children; + for (const auto &CommentInfo : Comments) { + json::Value Comment = serializeComment(*CommentInfo, Description); + // if a ParagraphComment is returned, then it is a top-level comment that + // needs to be inserted manually. + if (auto *ParagraphComment = Comment.getAsObject(); + ParagraphComment->get("ParagraphComment")) { + auto TextCommentsArray = extractTextComments(ParagraphComment); + insertComment(Description, TextCommentsArray, "ParagraphComments"); + } + } + Obj["Description"] = std::move(Description); } // Namespaces aren't SymbolInfos, so they dont have a DefLoc @@ -205,26 +277,32 @@ static void serializeReference(const Reference &Ref, Object &ReferenceObj) { ReferenceObj["Name"] = Ref.Name; ReferenceObj["QualName"] = Ref.QualName; ReferenceObj["USR"] = toHex(toStringRef(Ref.USR)); + if (!Ref.DocumentationFileName.empty()) + ReferenceObj["DocumentationFileName"] = Ref.DocumentationFileName; } // Although namespaces and records both have ScopeChildren, they serialize them // differently. Only enums, records, and typedefs are handled here. static void serializeCommonChildren(const ScopeChildren &Children, json::Object &Obj, - const std::optional &RepositoryUrl) { - static auto SerializeInfo = [&RepositoryUrl](const auto &Info, - Object &Object) { + const std::optional RepositoryUrl) { + static auto SerializeInfo = [RepositoryUrl](const auto &Info, + Object &Object) { serializeInfo(Info, Object, RepositoryUrl); }; - if (!Children.Enums.empty()) + if (!Children.Enums.empty()) { serializeArray(Children.Enums, Obj, "Enums", SerializeInfo); + Obj["HasEnums"] = true; + } if (!Children.Typedefs.empty()) serializeArray(Children.Typedefs, Obj, "Typedefs", SerializeInfo); - if (!Children.Records.empty()) + if (!Children.Records.empty()) { serializeArray(Children.Records, Obj, "Records", SerializeReferenceLambda); + Obj["HasRecords"] = true; + } } template @@ -234,10 +312,12 @@ static void serializeArray(const Container &Records, Object &Obj, json::Value RecordsArray = Array(); auto &RecordsArrayRef = *RecordsArray.getAsArray(); RecordsArrayRef.reserve(Records.size()); - for (const auto &Item : Records) { + for (size_t Index = 0; Index < Records.size(); ++Index) { json::Value ItemVal = Object(); auto &ItemObj = *ItemVal.getAsObject(); - SerializeInfo(Item, ItemObj); + SerializeInfo(Records[Index], ItemObj); + if (Index == Records.size() - 1) + ItemObj["End"] = true; RecordsArrayRef.push_back(ItemVal); } Obj[Key] = RecordsArray; @@ -304,7 +384,7 @@ static void serializeInfo(const FieldTypeInfo &I, Object &Obj) { } static void serializeInfo(const FunctionInfo &F, json::Object &Obj, - const std::optional &RepositoryURL) { + const std::optional RepositoryURL) { serializeCommonAttributes(F, Obj, RepositoryURL); Obj["IsStatic"] = F.IsStatic; @@ -380,6 +460,11 @@ static void serializeInfo(const FriendInfo &I, Object &Obj) { } } +static void insertArray(Object &Obj, json::Value &Array, StringRef Key) { + Obj[Key] = Array; + Obj["Has" + Key.str()] = true; +} + static void serializeInfo(const RecordInfo &I, json::Object &Obj, const std::optional &RepositoryUrl) { serializeCommonAttributes(I, Obj, RepositoryUrl); @@ -406,7 +491,7 @@ static void serializeInfo(const RecordInfo &I, json::Object &Obj, } if (!PubFunctionsArrayRef.empty()) - Obj["PublicFunctions"] = PubFunctionsArray; + insertArray(Obj, PubFunctionsArray, "PublicFunctions"); if (!ProtFunctionsArrayRef.empty()) Obj["ProtectedFunctions"] = ProtFunctionsArray; } @@ -430,7 +515,7 @@ static void serializeInfo(const RecordInfo &I, json::Object &Obj, } if (!PubMembersArrayRef.empty()) - Obj["PublicMembers"] = PublicMembersArray; + insertArray(Obj, PublicMembersArray, "PublicMembers"); if (!ProtMembersArrayRef.empty()) Obj["ProtectedMembers"] = ProtectedMembersArray; } @@ -459,7 +544,7 @@ static void serializeInfo(const RecordInfo &I, json::Object &Obj, } static void serializeInfo(const VarInfo &I, json::Object &Obj, - const std::optional &RepositoryUrl) { + const std::optional RepositoryUrl) { serializeCommonAttributes(I, Obj, RepositoryUrl); Obj["IsStatic"] = I.IsStatic; auto TypeObj = Object(); @@ -468,15 +553,15 @@ static void serializeInfo(const VarInfo &I, json::Object &Obj, } static void serializeInfo(const NamespaceInfo &I, json::Object &Obj, - const std::optional &RepositoryUrl) { + const std::optional RepositoryUrl) { serializeCommonAttributes(I, Obj, RepositoryUrl); if (!I.Children.Namespaces.empty()) serializeArray(I.Children.Namespaces, Obj, "Namespaces", SerializeReferenceLambda); - static auto SerializeInfo = [&RepositoryUrl](const auto &Info, - Object &Object) { + static auto SerializeInfo = [RepositoryUrl](const auto &Info, + Object &Object) { serializeInfo(Info, Object, RepositoryUrl); }; @@ -496,10 +581,7 @@ static SmallString<16> determineFileName(Info *I, SmallString<128> &Path) { SmallString<16> FileName; if (I->IT == InfoType::IT_record) { auto *RecordSymbolInfo = static_cast(I); - if (RecordSymbolInfo->MangledName.size() < 255) - FileName = RecordSymbolInfo->MangledName; - else - FileName = toStringRef(toHex(RecordSymbolInfo->USR)); + FileName = RecordSymbolInfo->MangledName; } else if (I->IT == InfoType::IT_namespace && I->Name != "") // Serialize the global namespace as index.json FileName = I->Name; @@ -527,7 +609,10 @@ Error JSONGenerator::generateDocs( } SmallString<16> FileName = determineFileName(Info, Path); + if (FileToInfos.contains(Path)) + continue; FileToInfos[Path].push_back(Info); + Info->DocumentationFileName = FileName; } for (const auto &Group : FileToInfos) { diff --git a/clang-tools-extra/clang-doc/Representation.cpp b/clang-tools-extra/clang-doc/Representation.cpp index beaf314a04ae1..79850e1f90253 100644 --- a/clang-tools-extra/clang-doc/Representation.cpp +++ b/clang-tools-extra/clang-doc/Representation.cpp @@ -247,6 +247,8 @@ void Reference::merge(Reference &&Other) { Name = Other.Name; if (Path.empty()) Path = Other.Path; + if (DocumentationFileName.empty()) + DocumentationFileName = Other.DocumentationFileName; } bool FriendInfo::mergeable(const FriendInfo &Other) { diff --git a/clang-tools-extra/clang-doc/Representation.h b/clang-tools-extra/clang-doc/Representation.h index 23f0e90daa27f..2a75f89696b7d 100644 --- a/clang-tools-extra/clang-doc/Representation.h +++ b/clang-tools-extra/clang-doc/Representation.h @@ -121,6 +121,10 @@ struct Reference { Reference(SymbolID USR, StringRef Name, InfoType IT, StringRef QualName, StringRef Path = StringRef()) : USR(USR), Name(Name), QualName(QualName), RefType(IT), Path(Path) {} + Reference(SymbolID USR, StringRef Name, InfoType IT, StringRef QualName, + StringRef Path, SmallString<16> DocumentationFileName) + : USR(USR), Name(Name), QualName(QualName), RefType(IT), Path(Path), + DocumentationFileName(DocumentationFileName) {} bool operator==(const Reference &Other) const { return std::tie(USR, Name, QualName, RefType) == @@ -155,6 +159,7 @@ struct Reference { // Path of directory where the clang-doc generated file will be saved // (possibly unresolved) llvm::SmallString<128> Path; + SmallString<16> DocumentationFileName; }; // Holds the children of a record or namespace. @@ -331,6 +336,11 @@ struct Info { llvm::SmallString<128> Path; // Path of directory where the clang-doc // generated file will be saved + // The name used for the file that this info is documented in. + // In the JSON generator, infos are documented in files with mangled names. + // Thus, we keep track of the physical filename for linking purposes. + SmallString<16> DocumentationFileName; + void mergeBase(Info &&I); bool mergeable(const Info &Other); diff --git a/clang-tools-extra/clang-doc/Serialize.cpp b/clang-tools-extra/clang-doc/Serialize.cpp index 7a0e00c6d9c2d..de73f68b09386 100644 --- a/clang-tools-extra/clang-doc/Serialize.cpp +++ b/clang-tools-extra/clang-doc/Serialize.cpp @@ -495,7 +495,8 @@ static void InsertChild(ScopeChildren &Scope, const NamespaceInfo &Info) { static void InsertChild(ScopeChildren &Scope, const RecordInfo &Info) { Scope.Records.emplace_back(Info.USR, Info.Name, InfoType::IT_record, - Info.Name, getInfoRelativePath(Info.Namespace)); + Info.Name, getInfoRelativePath(Info.Namespace), + Info.MangledName); } static void InsertChild(ScopeChildren &Scope, EnumInfo Info) { @@ -777,7 +778,13 @@ static void populateSymbolInfo(SymbolInfo &I, const T *D, const FullComment *C, Mangler->mangleCXXVTable(CXXD, MangledStream); else MangledStream << D->getNameAsString(); - I.MangledName = MangledName; + if (MangledName.size() > 255) + // File creation fails if the mangled name is too long, so default to the + // USR. We should look for a better check since filesystems differ in + // maximum filename length + I.MangledName = llvm::toStringRef(llvm::toHex(I.USR)); + else + I.MangledName = MangledName; delete Mangler; } diff --git a/clang-tools-extra/clang-doc/assets/clang-doc-mustache.css b/clang-tools-extra/clang-doc/assets/clang-doc-mustache.css index a885a36cb4a3d..e555ee7c370f7 100644 --- a/clang-tools-extra/clang-doc/assets/clang-doc-mustache.css +++ b/clang-tools-extra/clang-doc/assets/clang-doc-mustache.css @@ -469,3 +469,7 @@ a, a:visited, a:hover, a:active { text-decoration: none; color: inherit; } + +.code-block { + white-space: pre-line; +} diff --git a/clang-tools-extra/clang-doc/assets/class-template.mustache b/clang-tools-extra/clang-doc/assets/class-template.mustache index f9e78f5cd6bc9..b1a7470f7c33a 100644 --- a/clang-tools-extra/clang-doc/assets/class-template.mustache +++ b/clang-tools-extra/clang-doc/assets/class-template.mustache @@ -44,20 +44,20 @@
diff --git a/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp index 3c3024d538785..4b495e3877000 100644 --- a/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/InfiniteLoopCheck.cpp @@ -49,7 +49,7 @@ static Matcher loopEndingStmt(Matcher Internal) { } /// Return whether `Var` was changed in `LoopStmt`. -static bool isChanged(const Stmt *LoopStmt, const VarDecl *Var, +static bool isChanged(const Stmt *LoopStmt, const ValueDecl *Var, ASTContext *Context) { if (const auto *ForLoop = dyn_cast(LoopStmt)) return (ForLoop->getInc() && @@ -64,24 +64,35 @@ static bool isChanged(const Stmt *LoopStmt, const VarDecl *Var, return ExprMutationAnalyzer(*LoopStmt, *Context).isMutated(Var); } +static bool isVarPossiblyChanged(const Decl *Func, const Stmt *LoopStmt, + const ValueDecl *VD, ASTContext *Context) { + const VarDecl *Var = nullptr; + if (const auto *VarD = dyn_cast(VD)) { + Var = VarD; + } else if (const auto *BD = dyn_cast(VD)) { + if (const auto *DD = dyn_cast(BD->getDecomposedDecl())) + Var = DD; + } + + if (!Var) + return false; + + if (!Var->isLocalVarDeclOrParm() || Var->getType().isVolatileQualified()) + return true; + + if (!VD->getType().getTypePtr()->isIntegerType()) + return true; + + return hasPtrOrReferenceInFunc(Func, VD) || isChanged(LoopStmt, VD, Context); + // FIXME: Track references. +} + /// Return whether `Cond` is a variable that is possibly changed in `LoopStmt`. static bool isVarThatIsPossiblyChanged(const Decl *Func, const Stmt *LoopStmt, const Stmt *Cond, ASTContext *Context) { if (const auto *DRE = dyn_cast(Cond)) { - if (const auto *Var = dyn_cast(DRE->getDecl())) { - if (!Var->isLocalVarDeclOrParm()) - return true; - - if (Var->getType().isVolatileQualified()) - return true; - - if (!Var->getType().getTypePtr()->isIntegerType()) - return true; - - return hasPtrOrReferenceInFunc(Func, Var) || - isChanged(LoopStmt, Var, Context); - // FIXME: Track references. - } + if (const auto *VD = dyn_cast(DRE->getDecl())) + return isVarPossiblyChanged(Func, LoopStmt, VD, Context); } else if (isa(Cond)) { // FIXME: Handle MemberExpr. @@ -123,6 +134,10 @@ static std::string getCondVarNames(const Stmt *Cond) { if (const auto *DRE = dyn_cast(Cond)) { if (const auto *Var = dyn_cast(DRE->getDecl())) return std::string(Var->getName()); + + if (const auto *BD = dyn_cast(DRE->getDecl())) { + return std::string(BD->getName()); + } } std::string Result; @@ -214,10 +229,17 @@ static bool overlap(ArrayRef SCC, /// returns true iff `Cond` involves at least one static local variable. static bool hasStaticLocalVariable(const Stmt *Cond) { - if (const auto *DRE = dyn_cast(Cond)) + if (const auto *DRE = dyn_cast(Cond)) { if (const auto *VD = dyn_cast(DRE->getDecl())) if (VD->isStaticLocal()) return true; + + if (const auto *BD = dyn_cast(DRE->getDecl())) + if (const auto *DD = dyn_cast(BD->getDecomposedDecl())) + if (DD->isStaticLocal()) + return true; + } + for (const Stmt *Child : Cond->children()) if (Child && hasStaticLocalVariable(Child)) return true; diff --git a/clang-tools-extra/clang-tidy/bugprone/MoveForwardingReferenceCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/MoveForwardingReferenceCheck.cpp index 33642c407a3a9..bfa2ab51a6d03 100644 --- a/clang-tools-extra/clang-tidy/bugprone/MoveForwardingReferenceCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/MoveForwardingReferenceCheck.cpp @@ -45,7 +45,7 @@ static void replaceMoveWithForward(const UnresolvedLookupExpr *Callee, // We still conservatively put a "std::" in front of the forward because // we don't know whether the code also had a "using std::forward;". Diag << FixItHint::CreateReplacement(CallRange, "std::" + ForwardName); - } else if (const NamespaceDecl *Namespace = NNS->getAsNamespace()) { + } else if (const NamespaceBaseDecl *Namespace = NNS->getAsNamespace()) { if (Namespace->getName() == "std") { if (!NNS->getPrefix()) { // Called as "std::move". diff --git a/clang-tools-extra/clang-tidy/bugprone/SignedCharMisuseCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/SignedCharMisuseCheck.cpp index ea3b8b8e9df4f..dfd3cbfcd664a 100644 --- a/clang-tools-extra/clang-tidy/bugprone/SignedCharMisuseCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/SignedCharMisuseCheck.cpp @@ -74,17 +74,25 @@ void SignedCharMisuseCheck::registerMatchers(MatchFinder *Finder) { charCastExpression(true, IntegerType, "signedCastExpression"); const auto UnSignedCharCastExpr = charCastExpression(false, IntegerType, "unsignedCastExpression"); + const bool IsC23 = getLangOpts().C23; - // Catch assignments with signed char -> integer conversion. + // Catch assignments with signed char -> integer conversion. Ignore false + // positives on C23 enums with the fixed underlying type of signed char. const auto AssignmentOperatorExpr = expr(binaryOperator(hasOperatorName("="), hasLHS(hasType(IntegerType)), - hasRHS(SignedCharCastExpr))); + hasRHS(SignedCharCastExpr)), + IsC23 ? unless(binaryOperator( + hasLHS(hasType(hasCanonicalType(enumType()))))) + : Matcher(anything())); Finder->addMatcher(AssignmentOperatorExpr, this); - // Catch declarations with signed char -> integer conversion. - const auto Declaration = varDecl(isDefinition(), hasType(IntegerType), - hasInitializer(SignedCharCastExpr)); + // Catch declarations with signed char -> integer conversion. Ignore false + // positives on C23 enums with the fixed underlying type of signed char. + const auto Declaration = varDecl( + isDefinition(), hasType(IntegerType), hasInitializer(SignedCharCastExpr), + IsC23 ? unless(hasType(hasCanonicalType(enumType()))) + : Matcher(anything())); Finder->addMatcher(Declaration, this); diff --git a/clang-tools-extra/clang-tidy/bugprone/UnhandledSelfAssignmentCheck.cpp b/clang-tools-extra/clang-tidy/bugprone/UnhandledSelfAssignmentCheck.cpp index 1f432c4ccc5f0..c4c4267545b59 100644 --- a/clang-tools-extra/clang-tidy/bugprone/UnhandledSelfAssignmentCheck.cpp +++ b/clang-tools-extra/clang-tidy/bugprone/UnhandledSelfAssignmentCheck.cpp @@ -69,6 +69,28 @@ void UnhandledSelfAssignmentCheck::registerMatchers(MatchFinder *Finder) { cxxMethodDecl(unless(hasDescendant(cxxMemberCallExpr(callee(cxxMethodDecl( hasName("operator="), ofClass(equalsBoundNode("class")))))))); + // Checking that some kind of constructor is called and followed by a `swap`: + // T& operator=(const T& other) { + // T tmp{this->internal_data(), some, other, args}; + // swap(tmp); + // return *this; + // } + const auto HasCopyAndSwap = cxxMethodDecl( + ofClass(cxxRecordDecl()), + hasBody(compoundStmt( + hasDescendant( + varDecl(hasType(cxxRecordDecl(equalsBoundNode("class")))) + .bind("tmp_var")), + hasDescendant(stmt(anyOf( + cxxMemberCallExpr(hasArgument( + 0, declRefExpr(to(varDecl(equalsBoundNode("tmp_var")))))), + callExpr( + callee(functionDecl(hasName("swap"))), argumentCountIs(2), + hasAnyArgument( + declRefExpr(to(varDecl(equalsBoundNode("tmp_var"))))), + hasAnyArgument(unaryOperator(has(cxxThisExpr()), + hasOperatorName("*")))))))))); + DeclarationMatcher AdditionalMatcher = cxxMethodDecl(); if (WarnOnlyIfThisHasSuspiciousField) { // Matcher for standard smart pointers. @@ -89,14 +111,14 @@ void UnhandledSelfAssignmentCheck::registerMatchers(MatchFinder *Finder) { hasType(arrayType()))))))); } - Finder->addMatcher(cxxMethodDecl(ofClass(cxxRecordDecl().bind("class")), - isCopyAssignmentOperator(), IsUserDefined, - HasReferenceParam, HasNoSelfCheck, - unless(HasNonTemplateSelfCopy), - unless(HasTemplateSelfCopy), - HasNoNestedSelfAssign, AdditionalMatcher) - .bind("copyAssignmentOperator"), - this); + Finder->addMatcher( + cxxMethodDecl( + ofClass(cxxRecordDecl().bind("class")), isCopyAssignmentOperator(), + IsUserDefined, HasReferenceParam, HasNoSelfCheck, + unless(HasNonTemplateSelfCopy), unless(HasTemplateSelfCopy), + unless(HasCopyAndSwap), HasNoNestedSelfAssign, AdditionalMatcher) + .bind("copyAssignmentOperator"), + this); } void UnhandledSelfAssignmentCheck::check( diff --git a/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt b/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt index 3232f6e2cafe5..41386cdb55b1f 100644 --- a/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt +++ b/clang-tools-extra/clang-tidy/llvm/CMakeLists.txt @@ -11,6 +11,7 @@ add_clang_library(clangTidyLLVMModule STATIC PreferRegisterOverUnsignedCheck.cpp PreferStaticOverAnonymousNamespaceCheck.cpp TwineLocalCheck.cpp + UseNewMLIROpBuilderCheck.cpp LINK_LIBS clangTidy @@ -29,4 +30,5 @@ clang_target_link_libraries(clangTidyLLVMModule clangBasic clangLex clangTooling + clangTransformer ) diff --git a/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp b/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp index 075453046f0a1..c7c61fd1649cc 100644 --- a/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp +++ b/clang-tools-extra/clang-tidy/llvm/LLVMTidyModule.cpp @@ -18,6 +18,7 @@ #include "PreferRegisterOverUnsignedCheck.h" #include "PreferStaticOverAnonymousNamespaceCheck.h" #include "TwineLocalCheck.h" +#include "UseNewMLIROpBuilderCheck.h" namespace clang::tidy { namespace llvm_check { @@ -40,6 +41,8 @@ class LLVMModule : public ClangTidyModule { CheckFactories.registerCheck( "llvm-qualified-auto"); CheckFactories.registerCheck("llvm-twine-local"); + CheckFactories.registerCheck( + "llvm-use-new-mlir-op-builder"); } ClangTidyOptions getModuleOptions() override { diff --git a/clang-tools-extra/clang-tidy/llvm/UseNewMLIROpBuilderCheck.cpp b/clang-tools-extra/clang-tidy/llvm/UseNewMLIROpBuilderCheck.cpp new file mode 100644 index 0000000000000..0b28ea2508977 --- /dev/null +++ b/clang-tools-extra/clang-tidy/llvm/UseNewMLIROpBuilderCheck.cpp @@ -0,0 +1,133 @@ +//===--- UseNewMLIROpBuilderCheck.cpp - clang-tidy ------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "UseNewMLIROpBuilderCheck.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Basic/LLVM.h" +#include "clang/Lex/Lexer.h" +#include "clang/Tooling/Transformer/RangeSelector.h" +#include "clang/Tooling/Transformer/RewriteRule.h" +#include "clang/Tooling/Transformer/SourceCode.h" +#include "clang/Tooling/Transformer/Stencil.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FormatVariadic.h" + +namespace clang::tidy::llvm_check { +namespace { + +using namespace ::clang::ast_matchers; +using namespace ::clang::transformer; + +EditGenerator rewrite(RangeSelector Call, RangeSelector Builder, + RangeSelector CallArgs) { + // This is using an EditGenerator rather than ASTEdit as we want to warn even + // if in macro. + return [Call = std::move(Call), Builder = std::move(Builder), + CallArgs = + std::move(CallArgs)](const MatchFinder::MatchResult &Result) + -> Expected> { + Expected CallRange = Call(Result); + if (!CallRange) + return CallRange.takeError(); + SourceManager &SM = *Result.SourceManager; + const LangOptions &LangOpts = Result.Context->getLangOpts(); + SourceLocation Begin = CallRange->getBegin(); + + // This will result in just a warning and no edit. + bool InMacro = CallRange->getBegin().isMacroID(); + if (InMacro) { + while (SM.isMacroArgExpansion(Begin)) + Begin = SM.getImmediateExpansionRange(Begin).getBegin(); + Edit WarnOnly; + WarnOnly.Kind = EditKind::Range; + WarnOnly.Range = CharSourceRange::getCharRange(Begin, Begin); + return SmallVector({WarnOnly}); + } + + // This will try to extract the template argument as written so that the + // rewritten code looks closest to original. + auto NextToken = [&](std::optional CurrentToken) { + if (!CurrentToken) + return CurrentToken; + if (CurrentToken->getEndLoc() >= CallRange->getEnd()) + return std::optional(); + return clang::Lexer::findNextToken(CurrentToken->getLocation(), SM, + LangOpts); + }; + std::optional LessToken = + clang::Lexer::findNextToken(Begin, SM, LangOpts); + while (LessToken && LessToken->getKind() != clang::tok::less) { + LessToken = NextToken(LessToken); + } + if (!LessToken) { + return llvm::make_error(llvm::errc::invalid_argument, + "missing '<' token"); + } + std::optional EndToken = NextToken(LessToken); + for (std::optional GreaterToken = NextToken(EndToken); + GreaterToken && GreaterToken->getKind() != clang::tok::greater; + GreaterToken = NextToken(GreaterToken)) { + EndToken = GreaterToken; + } + if (!EndToken) { + return llvm::make_error(llvm::errc::invalid_argument, + "missing '>' token"); + } + + Expected BuilderRange = Builder(Result); + if (!BuilderRange) + return BuilderRange.takeError(); + Expected CallArgsRange = CallArgs(Result); + if (!CallArgsRange) + return CallArgsRange.takeError(); + + // Helper for concatting below. + auto GetText = [&](const CharSourceRange &Range) { + return clang::Lexer::getSourceText(Range, SM, LangOpts); + }; + + Edit Replace; + Replace.Kind = EditKind::Range; + Replace.Range = *CallRange; + std::string CallArgsStr; + // Only emit args if there are any. + if (auto CallArgsText = GetText(*CallArgsRange).ltrim(); + !CallArgsText.rtrim().empty()) { + CallArgsStr = llvm::formatv(", {}", CallArgsText); + } + Replace.Replacement = + llvm::formatv("{}::create({}{})", + GetText(CharSourceRange::getTokenRange( + LessToken->getEndLoc(), EndToken->getLastLoc())), + GetText(*BuilderRange), CallArgsStr); + + return SmallVector({Replace}); + }; +} + +RewriteRuleWith useNewMlirOpBuilderCheckRule() { + return makeRule( + cxxMemberCallExpr( + on(expr(hasType( + cxxRecordDecl(isSameOrDerivedFrom("::mlir::OpBuilder")))) + .bind("builder")), + callee(cxxMethodDecl(hasTemplateArgument(0, templateArgument()))), + callee(cxxMethodDecl(hasName("create")))) + .bind("call"), + rewrite(node("call"), node("builder"), callArgs("call")), + cat("use 'OpType::create(builder, ...)' instead of " + "'builder.create(...)'")); +} +} // namespace + +UseNewMlirOpBuilderCheck::UseNewMlirOpBuilderCheck(StringRef Name, + ClangTidyContext *Context) + : TransformerClangTidyCheck(useNewMlirOpBuilderCheckRule(), Name, Context) { +} + +} // namespace clang::tidy::llvm_check diff --git a/clang-tools-extra/clang-tidy/llvm/UseNewMLIROpBuilderCheck.h b/clang-tools-extra/clang-tidy/llvm/UseNewMLIROpBuilderCheck.h new file mode 100644 index 0000000000000..813a23c564782 --- /dev/null +++ b/clang-tools-extra/clang-tidy/llvm/UseNewMLIROpBuilderCheck.h @@ -0,0 +1,29 @@ +//===--- UseNewMLIROpBuilderCheck.h - clang-tidy ----------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_USENEWMLIROPBUILDERCHECK_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_USENEWMLIROPBUILDERCHECK_H + +#include "../utils/TransformerClangTidyCheck.h" + +namespace clang::tidy::llvm_check { + +/// Checks for uses of MLIR's old/to be deprecated `OpBuilder::create` form +/// and suggests using `T::create` instead. +class UseNewMlirOpBuilderCheck : public utils::TransformerClangTidyCheck { +public: + UseNewMlirOpBuilderCheck(StringRef Name, ClangTidyContext *Context); + + bool isLanguageVersionSupported(const LangOptions &LangOpts) const override { + return getLangOpts().CPlusPlus; + } +}; + +} // namespace clang::tidy::llvm_check + +#endif // LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_LLVM_USENEWMLIROPBUILDERCHECK_H diff --git a/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp b/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp index 21d443a1f34ff..1f6ceda9f5b9e 100644 --- a/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp +++ b/clang-tools-extra/clang-tidy/misc/HeaderIncludeCycleCheck.cpp @@ -13,8 +13,8 @@ #include "clang/Lex/Preprocessor.h" #include "llvm/Support/Regex.h" #include -#include #include +#include using namespace clang::ast_matchers; @@ -23,8 +23,8 @@ namespace clang::tidy::misc { namespace { struct Include { - FileID Id; - llvm::StringRef Name; + const FileEntry *File; + StringRef Name; SourceLocation Loc; }; @@ -50,31 +50,27 @@ class CyclicDependencyCallbacks : public PPCallbacks { if (Reason != EnterFile && Reason != ExitFile) return; - FileID Id = SM.getFileID(Loc); + const FileID Id = SM.getFileID(Loc); if (Id.isInvalid()) return; + const FileEntry *NewFile = SM.getFileEntryForID(Id); + const FileEntry *PrevFile = SM.getFileEntryForID(PrevFID); + if (Reason == ExitFile) { - if ((Files.size() > 1U) && (Files.back().Id == PrevFID) && - (Files[Files.size() - 2U].Id == Id)) + if ((Files.size() > 1U) && (Files.back().File == PrevFile) && + (Files[Files.size() - 2U].File == NewFile)) Files.pop_back(); return; } - if (!Files.empty() && Files.back().Id == Id) + if (!Files.empty() && Files.back().File == NewFile) return; - std::optional FilePath = SM.getNonBuiltinFilenameForID(Id); - llvm::StringRef FileName = - FilePath ? llvm::sys::path::filename(*FilePath) : llvm::StringRef(); - - if (!NextToEnter) - NextToEnter = Include{Id, FileName, SourceLocation()}; - - assert(NextToEnter->Name == FileName); - NextToEnter->Id = Id; - Files.emplace_back(*NextToEnter); - NextToEnter.reset(); + const std::optional FilePath = SM.getNonBuiltinFilenameForID(Id); + const StringRef FileName = + FilePath ? llvm::sys::path::filename(*FilePath) : StringRef(); + Files.push_back({NewFile, FileName, std::exchange(NextToEnter, {})}); } void InclusionDirective(SourceLocation, const Token &, StringRef FilePath, @@ -85,36 +81,26 @@ class CyclicDependencyCallbacks : public PPCallbacks { if (FileType != clang::SrcMgr::C_User) return; - llvm::StringRef FileName = llvm::sys::path::filename(FilePath); - NextToEnter = {FileID(), FileName, Range.getBegin()}; + NextToEnter = Range.getBegin(); if (!File) return; - FileID Id = SM.translateFile(*File); - if (Id.isInvalid()) - return; - - checkForDoubleInclude(Id, FileName, Range.getBegin()); - } - - void EndOfMainFile() override { - if (!Files.empty() && Files.back().Id == SM.getMainFileID()) - Files.pop_back(); - - assert(Files.empty()); + checkForDoubleInclude(&File->getFileEntry(), + llvm::sys::path::filename(FilePath), + Range.getBegin()); } - void checkForDoubleInclude(FileID Id, llvm::StringRef FileName, + void checkForDoubleInclude(const FileEntry *File, StringRef FileName, SourceLocation Loc) { - auto It = - std::find_if(Files.rbegin(), Files.rend(), - [&](const Include &Entry) { return Entry.Id == Id; }); + const auto It = + llvm::find_if(llvm::reverse(Files), + [&](const Include &Entry) { return Entry.File == File; }); if (It == Files.rend()) return; - const std::optional FilePath = SM.getNonBuiltinFilenameForID(Id); - if (!FilePath || isFileIgnored(*FilePath)) + const StringRef FilePath = File->tryGetRealPathName(); + if (FilePath.empty() || isFileIgnored(FilePath)) return; if (It == Files.rbegin()) { @@ -144,9 +130,19 @@ class CyclicDependencyCallbacks : public PPCallbacks { }); } +#ifndef NDEBUG + void EndOfMainFile() override { + if (!Files.empty() && + Files.back().File == SM.getFileEntryForID(SM.getMainFileID())) + Files.pop_back(); + + assert(Files.empty()); + } +#endif + private: - std::deque Files; - std::optional NextToEnter; + std::vector Files; + SourceLocation NextToEnter; HeaderIncludeCycleCheck &Check; const SourceManager &SM; std::vector IgnoredFilesRegexes; diff --git a/clang-tools-extra/clang-tidy/misc/UnusedAliasDeclsCheck.cpp b/clang-tools-extra/clang-tidy/misc/UnusedAliasDeclsCheck.cpp index 2dfaca19a8981..86992cd8a141b 100644 --- a/clang-tools-extra/clang-tidy/misc/UnusedAliasDeclsCheck.cpp +++ b/clang-tools-extra/clang-tidy/misc/UnusedAliasDeclsCheck.cpp @@ -36,7 +36,8 @@ void UnusedAliasDeclsCheck::check(const MatchFinder::MatchResult &Result) { if (const auto *NestedName = Result.Nodes.getNodeAs("nns")) { - if (const auto *AliasDecl = NestedName->getAsNamespaceAlias()) { + if (const auto *AliasDecl = dyn_cast_if_present( + NestedName->getAsNamespace())) { FoundDecls[AliasDecl] = CharSourceRange(); } } diff --git a/clang-tools-extra/clang-tidy/modernize/UseDesignatedInitializersCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseDesignatedInitializersCheck.cpp index 7ea9676b13ec0..bb8fb2404a9a5 100644 --- a/clang-tools-extra/clang-tidy/modernize/UseDesignatedInitializersCheck.cpp +++ b/clang-tools-extra/clang-tidy/modernize/UseDesignatedInitializersCheck.cpp @@ -121,10 +121,11 @@ void UseDesignatedInitializersCheck::registerMatchers(MatchFinder *Finder) { hasAnyBase(hasType(cxxRecordDecl(has(fieldDecl())))); Finder->addMatcher( initListExpr( - hasType(cxxRecordDecl( - RestrictToPODTypes ? isPOD() : isAggregate(), - unless(anyOf(HasBaseWithFields, hasName("::std::array")))) - .bind("type")), + hasType(hasUnqualifiedDesugaredType(recordType(hasDeclaration( + cxxRecordDecl( + RestrictToPODTypes ? isPOD() : isAggregate(), + unless(anyOf(HasBaseWithFields, hasName("::std::array")))) + .bind("type"))))), IgnoreSingleElementAggregates ? hasMoreThanOneElement() : anything(), unless(isFullyDesignated())) .bind("init"), @@ -155,7 +156,7 @@ void UseDesignatedInitializersCheck::check( DiagnosticBuilder Diag = diag(InitList->getLBraceLoc(), "use designated initializer list to initialize %0"); - Diag << Type << InitList->getSourceRange(); + Diag << InitList->getType() << InitList->getSourceRange(); for (const Stmt *InitExpr : *SyntacticInitList) { const auto Designator = Designators[InitExpr->getBeginLoc()]; if (Designator && !Designator->empty()) diff --git a/clang-tools-extra/clang-tidy/modernize/UseUsingCheck.cpp b/clang-tools-extra/clang-tidy/modernize/UseUsingCheck.cpp index 936a906651f16..e307339bd83aa 100644 --- a/clang-tools-extra/clang-tidy/modernize/UseUsingCheck.cpp +++ b/clang-tools-extra/clang-tidy/modernize/UseUsingCheck.cpp @@ -92,7 +92,7 @@ void UseUsingCheck::check(const MatchFinder::MatchResult &Result) { const LangOptions &LO = getLangOpts(); // Match CXXRecordDecl only to store the range of the last non-implicit full - // declaration, to later check whether it's within the typdef itself. + // declaration, to later check whether it's within the typedef itself. const auto *MatchedTagDecl = Result.Nodes.getNodeAs(TagDeclName); if (MatchedTagDecl) { // It is not sufficient to just track the last TagDecl that we've seen, @@ -152,7 +152,7 @@ void UseUsingCheck::check(const MatchFinder::MatchResult &Result) { StringRef ExtraReference = ""; if (MainTypeEndLoc.isValid() && TypeRange.fullyContains(MainTypeEndLoc)) { // Each type introduced in a typedef can specify being a reference or - // pointer type seperately, so we need to sigure out if the new using-decl + // pointer type separately, so we need to figure out if the new using-decl // needs to be to a reference or pointer as well. const SourceLocation Tok = utils::lexer::findPreviousAnyTokenKind( MatchedDecl->getLocation(), SM, LO, tok::TokenKind::star, diff --git a/clang-tools-extra/clang-tidy/portability/TemplateVirtualMemberFunctionCheck.cpp b/clang-tools-extra/clang-tidy/portability/TemplateVirtualMemberFunctionCheck.cpp index 9c2f27f01f184..aaa23367a3825 100644 --- a/clang-tools-extra/clang-tidy/portability/TemplateVirtualMemberFunctionCheck.cpp +++ b/clang-tools-extra/clang-tidy/portability/TemplateVirtualMemberFunctionCheck.cpp @@ -18,10 +18,11 @@ AST_MATCHER(CXXMethodDecl, isUsed) { return Node.isUsed(); } void TemplateVirtualMemberFunctionCheck::registerMatchers(MatchFinder *Finder) { Finder->addMatcher( - cxxMethodDecl(ofClass(classTemplateSpecializationDecl( + cxxMethodDecl(isVirtual(), + ofClass(classTemplateSpecializationDecl( unless(isExplicitTemplateSpecialization())) .bind("specialization")), - isVirtual(), unless(isUsed()), + unless(isUsed()), unless(isPure()), unless(cxxDestructorDecl(isDefaulted()))) .bind("method"), this); diff --git a/clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py b/clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py index 0f8ac7344aca3..7cd21afd70f7e 100755 --- a/clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py +++ b/clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py @@ -177,7 +177,7 @@ def main(): parser.add_argument( "-j", type=int, - default=1, + default=0, help="number of tidy instances to be run in parallel.", ) parser.add_argument( @@ -318,6 +318,7 @@ def main(): if max_task_count == 0: max_task_count = multiprocessing.cpu_count() max_task_count = min(len(lines_by_file), max_task_count) + print(f"Running clang-tidy in {max_task_count} threads...") combine_fixes = False export_fixes_dir = None diff --git a/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py b/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py index 8741147a4f8a3..a3dca6c57571c 100755 --- a/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py +++ b/clang-tools-extra/clang-tidy/tool/run-clang-tidy.py @@ -548,7 +548,7 @@ async def main() -> None: files = {f for f in files if file_name_re.search(f)} print( - "Running clang-tidy for", + f"Running clang-tidy in {max_task} threads for", len(files), "files out of", number_files_in_database, diff --git a/clang-tools-extra/clang-tidy/utils/Aliasing.cpp b/clang-tools-extra/clang-tidy/utils/Aliasing.cpp index 2facf0625605e..cbe4873b5c022 100644 --- a/clang-tools-extra/clang-tidy/utils/Aliasing.cpp +++ b/clang-tools-extra/clang-tidy/utils/Aliasing.cpp @@ -14,14 +14,14 @@ namespace clang::tidy::utils { /// Return whether \p S is a reference to the declaration of \p Var. -static bool isAccessForVar(const Stmt *S, const VarDecl *Var) { +static bool isAccessForVar(const Stmt *S, const ValueDecl *Var) { if (const auto *DRE = dyn_cast(S)) return DRE->getDecl() == Var; return false; } -static bool capturesByRef(const CXXRecordDecl *RD, const VarDecl *Var) { +static bool capturesByRef(const CXXRecordDecl *RD, const ValueDecl *Var) { return llvm::any_of(RD->captures(), [Var](const LambdaCapture &C) { return C.capturesVariable() && C.getCaptureKind() == LCK_ByRef && C.getCapturedVar() == Var; @@ -29,9 +29,9 @@ static bool capturesByRef(const CXXRecordDecl *RD, const VarDecl *Var) { } /// Return whether \p Var has a pointer or reference in \p S. -static bool isPtrOrReferenceForVar(const Stmt *S, const VarDecl *Var) { +static bool isPtrOrReferenceForVar(const Stmt *S, const ValueDecl *Var) { // Treat block capture by reference as a form of taking a reference. - if (Var->isEscapingByref()) + if (const auto *VD = dyn_cast(Var); VD && VD->isEscapingByref()) return true; if (const auto *DS = dyn_cast(S)) { @@ -61,7 +61,7 @@ static bool isPtrOrReferenceForVar(const Stmt *S, const VarDecl *Var) { } /// Return whether \p Var has a pointer or reference in \p S. -static bool hasPtrOrReferenceInStmt(const Stmt *S, const VarDecl *Var) { +static bool hasPtrOrReferenceInStmt(const Stmt *S, const ValueDecl *Var) { if (isPtrOrReferenceForVar(S, Var)) return true; @@ -77,7 +77,7 @@ static bool hasPtrOrReferenceInStmt(const Stmt *S, const VarDecl *Var) { } static bool refersToEnclosingLambdaCaptureByRef(const Decl *Func, - const VarDecl *Var) { + const ValueDecl *Var) { const auto *MD = dyn_cast(Func); if (!MD) return false; @@ -89,7 +89,7 @@ static bool refersToEnclosingLambdaCaptureByRef(const Decl *Func, return capturesByRef(RD, Var); } -bool hasPtrOrReferenceInFunc(const Decl *Func, const VarDecl *Var) { +bool hasPtrOrReferenceInFunc(const Decl *Func, const ValueDecl *Var) { return hasPtrOrReferenceInStmt(Func->getBody(), Var) || refersToEnclosingLambdaCaptureByRef(Func, Var); } diff --git a/clang-tools-extra/clang-tidy/utils/Aliasing.h b/clang-tools-extra/clang-tidy/utils/Aliasing.h index 7dad16fc57f1e..6c0763b766805 100644 --- a/clang-tools-extra/clang-tidy/utils/Aliasing.h +++ b/clang-tools-extra/clang-tidy/utils/Aliasing.h @@ -25,7 +25,7 @@ namespace clang::tidy::utils { /// For `f()` and `n` the function returns ``true`` because `p` is a /// pointer to `n` created in `f()`. -bool hasPtrOrReferenceInFunc(const Decl *Func, const VarDecl *Var); +bool hasPtrOrReferenceInFunc(const Decl *Func, const ValueDecl *Var); } // namespace clang::tidy::utils diff --git a/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp b/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp index 7f4ccca84faa5..e1c1bee97f6d4 100644 --- a/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp +++ b/clang-tools-extra/clang-tidy/utils/FormatStringConverter.cpp @@ -207,13 +207,9 @@ FormatStringConverter::FormatStringConverter( ArgsOffset(FormatArgOffset + 1), LangOpts(LO) { assert(ArgsOffset <= NumArgs); FormatExpr = llvm::dyn_cast( - Args[FormatArgOffset]->IgnoreImplicitAsWritten()); + Args[FormatArgOffset]->IgnoreUnlessSpelledInSource()); - if (!FormatExpr || !FormatExpr->isOrdinary()) { - // Function must have a narrow string literal as its first argument. - conversionNotPossible("first argument is not a narrow string literal"); - return; - } + assert(FormatExpr && FormatExpr->isOrdinary()); if (const std::optional MaybeMacroName = formatStringContainsUnreplaceableMacro(Call, FormatExpr, SM, PP); diff --git a/clang-tools-extra/clang-tidy/utils/RenamerClangTidyCheck.cpp b/clang-tools-extra/clang-tidy/utils/RenamerClangTidyCheck.cpp index 6cf38ddf3d914..dd28806e008ed 100644 --- a/clang-tools-extra/clang-tidy/utils/RenamerClangTidyCheck.cpp +++ b/clang-tools-extra/clang-tidy/utils/RenamerClangTidyCheck.cpp @@ -282,7 +282,8 @@ class RenamerClangTidyVisitor bool TraverseNestedNameSpecifierLoc(NestedNameSpecifierLoc Loc) { if (const NestedNameSpecifier *Spec = Loc.getNestedNameSpecifier()) { - if (const NamespaceDecl *Decl = Spec->getAsNamespace()) + if (const auto *Decl = + dyn_cast_if_present(Spec->getAsNamespace())) Check->addUsage(Decl, Loc.getLocalSourceRange(), SM); } diff --git a/clang-tools-extra/clangd/AST.cpp b/clang-tools-extra/clangd/AST.cpp index e274236527817..f2631e5abb6a3 100644 --- a/clang-tools-extra/clangd/AST.cpp +++ b/clang-tools-extra/clangd/AST.cpp @@ -666,12 +666,14 @@ std::string getQualification(ASTContext &Context, return getQualification( Context, DestContext, ND->getDeclContext(), [&](NestedNameSpecifier *NNS) { - if (NNS->getKind() != NestedNameSpecifier::Namespace) + const NamespaceDecl *NS = + dyn_cast_if_present(NNS->getAsNamespace()); + if (!NS) return false; - const auto *CanonNSD = NNS->getAsNamespace()->getCanonicalDecl(); + NS = NS->getCanonicalDecl(); return llvm::any_of(VisibleNamespaceDecls, - [CanonNSD](const NamespaceDecl *NSD) { - return NSD->getCanonicalDecl() == CanonNSD; + [NS](const NamespaceDecl *NSD) { + return NSD->getCanonicalDecl() == NS; }); }); } diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp index a703009e2b467..b445dcf2bbd2e 100644 --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -1279,11 +1279,9 @@ void ClangdLSPServer::onHover(const TextDocumentPositionParams &Params, R.contents.kind = HoverContentFormat; R.range = (*H)->SymRange; switch (HoverContentFormat) { - case MarkupKind::PlainText: - R.contents.value = (*H)->present().asPlainText(); - return Reply(std::move(R)); case MarkupKind::Markdown: - R.contents.value = (*H)->present().asMarkdown(); + case MarkupKind::PlainText: + R.contents.value = (*H)->present(HoverContentFormat); return Reply(std::move(R)); }; llvm_unreachable("unhandled MarkupKind"); diff --git a/clang-tools-extra/clangd/CodeComplete.cpp b/clang-tools-extra/clangd/CodeComplete.cpp index d5907e3143bf6..9c17b4ca9b706 100644 --- a/clang-tools-extra/clangd/CodeComplete.cpp +++ b/clang-tools-extra/clangd/CodeComplete.cpp @@ -193,7 +193,11 @@ MarkupContent renderDoc(const markup::Document &Doc, MarkupKind Kind) { Result.value.append(Doc.asPlainText()); break; case MarkupKind::Markdown: - Result.value.append(Doc.asMarkdown()); + if (Config::current().Documentation.CommentFormat == + Config::CommentFormatPolicy::PlainText) + Result.value.append(Doc.asEscapedMarkdown()); + else + Result.value.append(Doc.asMarkdown()); break; } return Result; @@ -1470,7 +1474,6 @@ bool allowIndex(CodeCompletionContext &CC) { switch (NameSpec->getKind()) { case NestedNameSpecifier::Global: case NestedNameSpecifier::Namespace: - case NestedNameSpecifier::NamespaceAlias: return true; case NestedNameSpecifier::Super: case NestedNameSpecifier::TypeSpec: diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h index 83e0fce847271..2e3e0a431ab1f 100644 --- a/clang-tools-extra/clangd/Config.h +++ b/clang-tools-extra/clangd/Config.h @@ -196,6 +196,19 @@ struct Config { /// Controls highlighting modifiers that are disabled. std::vector DisabledModifiers; } SemanticTokens; + + enum class CommentFormatPolicy { + /// Treat comments as plain text. + PlainText, + /// Treat comments as Markdown. + Markdown, + /// Treat comments as doxygen. + Doxygen, + }; + + struct { + CommentFormatPolicy CommentFormat = CommentFormatPolicy::PlainText; + } Documentation; }; } // namespace clangd diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp index 35d35f747a868..5dda6dd8bf712 100644 --- a/clang-tools-extra/clangd/ConfigCompile.cpp +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -198,6 +198,7 @@ struct FragmentCompiler { compile(std::move(F.InlayHints)); compile(std::move(F.SemanticTokens)); compile(std::move(F.Style)); + compile(std::move(F.Documentation)); } void compile(Fragment::IfBlock &&F) { @@ -793,6 +794,21 @@ struct FragmentCompiler { } } + void compile(Fragment::DocumentationBlock &&F) { + if (F.CommentFormat) { + if (auto Val = + compileEnum("CommentFormat", + *F.CommentFormat) + .map("Plaintext", Config::CommentFormatPolicy::PlainText) + .map("Markdown", Config::CommentFormatPolicy::Markdown) + .map("Doxygen", Config::CommentFormatPolicy::Doxygen) + .value()) + Out.Apply.push_back([Val](const Params &, Config &C) { + C.Documentation.CommentFormat = *Val; + }); + } + } + constexpr static llvm::SourceMgr::DiagKind Error = llvm::SourceMgr::DK_Error; constexpr static llvm::SourceMgr::DiagKind Warning = llvm::SourceMgr::DK_Warning; diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h index 9e00dbc9dc90a..a6a7cd53fb9bf 100644 --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -393,6 +393,17 @@ struct Fragment { std::vector> DisabledModifiers; }; SemanticTokensBlock SemanticTokens; + + /// Configures documentation style and behaviour. + struct DocumentationBlock { + /// Specifies the format of comments in the code. + /// Valid values are enum Config::CommentFormatPolicy values: + /// - Plaintext: Treat comments as plain text. + /// - Markdown: Treat comments as Markdown. + /// - Doxygen: Treat comments as doxygen. + std::optional> CommentFormat; + }; + DocumentationBlock Documentation; }; } // namespace config diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp index 6086357d8f0d9..289b7e8d1c49d 100644 --- a/clang-tools-extra/clangd/ConfigYAML.cpp +++ b/clang-tools-extra/clangd/ConfigYAML.cpp @@ -69,6 +69,7 @@ class Parser { Dict.handle("Hover", [&](Node &N) { parse(F.Hover, N); }); Dict.handle("InlayHints", [&](Node &N) { parse(F.InlayHints, N); }); Dict.handle("SemanticTokens", [&](Node &N) { parse(F.SemanticTokens, N); }); + Dict.handle("Documentation", [&](Node &N) { parse(F.Documentation, N); }); Dict.parse(N); return !(N.failed() || HadError); } @@ -312,6 +313,15 @@ class Parser { Dict.parse(N); } + void parse(Fragment::DocumentationBlock &F, Node &N) { + DictParser Dict("Documentation", this); + Dict.handle("CommentFormat", [&](Node &N) { + if (auto Value = scalarValue(N, "CommentFormat")) + F.CommentFormat = *Value; + }); + Dict.parse(N); + } + // Helper for parsing mapping nodes (dictionaries). // We don't use YamlIO as we want to control over unknown keys. class DictParser { diff --git a/clang-tools-extra/clangd/DumpAST.cpp b/clang-tools-extra/clangd/DumpAST.cpp index 8f24477ecd3de..c6075e75e9a6b 100644 --- a/clang-tools-extra/clangd/DumpAST.cpp +++ b/clang-tools-extra/clangd/DumpAST.cpp @@ -158,7 +158,6 @@ class DumpVisitor : public RecursiveASTVisitor { NNS_KIND(TypeSpec); NNS_KIND(Global); NNS_KIND(Super); - NNS_KIND(NamespaceAlias); #undef NNS_KIND } llvm_unreachable("Unhandled SpecifierKind enum"); @@ -281,8 +280,6 @@ class DumpVisitor : public RecursiveASTVisitor { return NNS.getAsIdentifier()->getName().str() + "::"; case NestedNameSpecifier::Namespace: return NNS.getAsNamespace()->getNameAsString() + "::"; - case NestedNameSpecifier::NamespaceAlias: - return NNS.getAsNamespaceAlias()->getNameAsString() + "::"; default: return ""; } diff --git a/clang-tools-extra/clangd/FindTarget.cpp b/clang-tools-extra/clangd/FindTarget.cpp index 91fd3b0f8567b..b1089577ba819 100644 --- a/clang-tools-extra/clangd/FindTarget.cpp +++ b/clang-tools-extra/clangd/FindTarget.cpp @@ -491,9 +491,6 @@ struct TargetFinder { case NestedNameSpecifier::Namespace: add(NNS->getAsNamespace(), Flags); return; - case NestedNameSpecifier::NamespaceAlias: - add(NNS->getAsNamespaceAlias(), Flags); - return; case NestedNameSpecifier::Identifier: if (Resolver) { add(Resolver->resolveNestedNameSpecifierToType(NNS), Flags); diff --git a/clang-tools-extra/clangd/Hover.cpp b/clang-tools-extra/clangd/Hover.cpp index e39d8bfcf83f3..1e0718d673260 100644 --- a/clang-tools-extra/clangd/Hover.cpp +++ b/clang-tools-extra/clangd/Hover.cpp @@ -15,6 +15,7 @@ #include "Headers.h" #include "IncludeCleaner.h" #include "ParsedAST.h" +#include "Protocol.h" #include "Selection.h" #include "SourceCode.h" #include "clang-include-cleaner/Analysis.h" @@ -960,42 +961,6 @@ std::optional getHoverContents(const Attr *A, ParsedAST &AST) { return HI; } -bool isParagraphBreak(llvm::StringRef Rest) { - return Rest.ltrim(" \t").starts_with("\n"); -} - -bool punctuationIndicatesLineBreak(llvm::StringRef Line) { - constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt"; - - Line = Line.rtrim(); - return !Line.empty() && Punctuation.contains(Line.back()); -} - -bool isHardLineBreakIndicator(llvm::StringRef Rest) { - // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote, - // '#' headings, '`' code blocks - constexpr llvm::StringLiteral LinebreakIndicators = R"txt(-*@\>#`)txt"; - - Rest = Rest.ltrim(" \t"); - if (Rest.empty()) - return false; - - if (LinebreakIndicators.contains(Rest.front())) - return true; - - if (llvm::isDigit(Rest.front())) { - llvm::StringRef AfterDigit = Rest.drop_while(llvm::isDigit); - if (AfterDigit.starts_with(".") || AfterDigit.starts_with(")")) - return true; - } - return false; -} - -bool isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest) { - // Should we also consider whether Line is short? - return punctuationIndicatesLineBreak(Line) || isHardLineBreakIndicator(Rest); -} - void addLayoutInfo(const NamedDecl &ND, HoverInfo &HI) { if (ND.isInvalidDecl()) return; @@ -1570,6 +1535,26 @@ markup::Document HoverInfo::present() const { return Output; } +std::string HoverInfo::present(MarkupKind Kind) const { + if (Kind == MarkupKind::Markdown) { + const Config &Cfg = Config::current(); + if ((Cfg.Documentation.CommentFormat == + Config::CommentFormatPolicy::Markdown) || + (Cfg.Documentation.CommentFormat == + Config::CommentFormatPolicy::Doxygen)) + // If the user prefers Markdown, we use the present() method to generate + // the Markdown output. + return present().asMarkdown(); + if (Cfg.Documentation.CommentFormat == + Config::CommentFormatPolicy::PlainText) + // If the user prefers plain text, we use the present() method to generate + // the plain text output. + return present().asEscapedMarkdown(); + } + + return present().asPlainText(); +} + // If the backtick at `Offset` starts a probable quoted range, return the range // (including the quotes). std::optional getBacktickQuoteRange(llvm::StringRef Line, @@ -1583,9 +1568,14 @@ std::optional getBacktickQuoteRange(llvm::StringRef Line, return std::nullopt; // The quoted string must be nonempty and usually has no leading/trailing ws. - auto Next = Line.find('`', Offset + 1); + auto Next = Line.find_first_of("`\n", Offset + 1); if (Next == llvm::StringRef::npos) return std::nullopt; + + // There should be no newline in the quoted string. + if (Line[Next] == '\n') + return std::nullopt; + llvm::StringRef Contents = Line.slice(Offset + 1, Next); if (Contents.empty() || isWhitespace(Contents.front()) || isWhitespace(Contents.back())) @@ -1600,51 +1590,40 @@ std::optional getBacktickQuoteRange(llvm::StringRef Line, return Line.slice(Offset, Next + 1); } -void parseDocumentationLine(llvm::StringRef Line, markup::Paragraph &Out) { +void parseDocumentationParagraph(llvm::StringRef Text, markup::Paragraph &Out) { // Probably this is appendText(Line), but scan for something interesting. - for (unsigned I = 0; I < Line.size(); ++I) { - switch (Line[I]) { + for (unsigned I = 0; I < Text.size(); ++I) { + switch (Text[I]) { case '`': - if (auto Range = getBacktickQuoteRange(Line, I)) { - Out.appendText(Line.substr(0, I)); + if (auto Range = getBacktickQuoteRange(Text, I)) { + Out.appendText(Text.substr(0, I)); Out.appendCode(Range->trim("`"), /*Preserve=*/true); - return parseDocumentationLine(Line.substr(I + Range->size()), Out); + return parseDocumentationParagraph(Text.substr(I + Range->size()), Out); } break; } } - Out.appendText(Line).appendSpace(); + Out.appendText(Text); } void parseDocumentation(llvm::StringRef Input, markup::Document &Output) { - std::vector ParagraphLines; - auto FlushParagraph = [&] { - if (ParagraphLines.empty()) - return; - auto &P = Output.addParagraph(); - for (llvm::StringRef Line : ParagraphLines) - parseDocumentationLine(Line, P); - ParagraphLines.clear(); - }; - - llvm::StringRef Line, Rest; - for (std::tie(Line, Rest) = Input.split('\n'); - !(Line.empty() && Rest.empty()); - std::tie(Line, Rest) = Rest.split('\n')) { - - // After a linebreak remove spaces to avoid 4 space markdown code blocks. - // FIXME: make FlushParagraph handle this. - Line = Line.ltrim(); - if (!Line.empty()) - ParagraphLines.push_back(Line); - - if (isParagraphBreak(Rest) || isHardLineBreakAfter(Line, Rest)) { - FlushParagraph(); - } + // A documentation string is treated as a sequence of paragraphs, + // where the paragraphs are seperated by at least one empty line + // (meaning 2 consecutive newline characters). + // Possible leading empty lines (introduced by an odd number > 1 of + // empty lines between 2 paragraphs) will be removed later in the Markup + // renderer. + llvm::StringRef Paragraph, Rest; + for (std::tie(Paragraph, Rest) = Input.split("\n\n"); + !(Paragraph.empty() && Rest.empty()); + std::tie(Paragraph, Rest) = Rest.split("\n\n")) { + + // The Paragraph will be empty if there is an even number of newline + // characters between two paragraphs, so we skip it. + if (!Paragraph.empty()) + parseDocumentationParagraph(Paragraph, Output.addParagraph()); } - FlushParagraph(); } - llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const HoverInfo::PrintedType &T) { OS << T.Type; diff --git a/clang-tools-extra/clangd/Hover.h b/clang-tools-extra/clangd/Hover.h index fe689de44732e..2f65431bd72de 100644 --- a/clang-tools-extra/clangd/Hover.h +++ b/clang-tools-extra/clangd/Hover.h @@ -120,6 +120,8 @@ struct HoverInfo { /// Produce a user-readable information. markup::Document present() const; + + std::string present(MarkupKind Kind) const; }; inline bool operator==(const HoverInfo::PrintedType &LHS, diff --git a/clang-tools-extra/clangd/IncludeFixer.cpp b/clang-tools-extra/clangd/IncludeFixer.cpp index 4ff021c4c390a..50bc2bd7ccb94 100644 --- a/clang-tools-extra/clangd/IncludeFixer.cpp +++ b/clang-tools-extra/clangd/IncludeFixer.cpp @@ -403,25 +403,27 @@ std::optional extractUnresolvedNameCheaply( if (auto *Nested = SS->getScopeRep()) { if (Nested->getKind() == NestedNameSpecifier::Global) { Result.ResolvedScope = ""; - } else if (const auto *NS = Nested->getAsNamespace()) { - std::string SpecifiedNS = printNamespaceScope(*NS); - std::optional Spelling = getSpelledSpecifier(*SS, SM); - - // Check the specifier spelled in the source. - // If the resolved scope doesn't end with the spelled scope, the - // resolved scope may come from a sema typo correction. For example, - // sema assumes that "clangd::" is a typo of "clang::" and uses - // "clang::" as the specified scope in: - // namespace clang { clangd::X; } - // In this case, we use the "typo" specifier as extra scope instead - // of using the scope assumed by sema. - if (!Spelling || llvm::StringRef(SpecifiedNS).ends_with(*Spelling)) { - Result.ResolvedScope = std::move(SpecifiedNS); + } else if (const NamespaceBaseDecl *NSB = Nested->getAsNamespace()) { + if (const auto *NS = dyn_cast(NSB)) { + std::string SpecifiedNS = printNamespaceScope(*NS); + std::optional Spelling = getSpelledSpecifier(*SS, SM); + + // Check the specifier spelled in the source. + // If the resolved scope doesn't end with the spelled scope, the + // resolved scope may come from a sema typo correction. For example, + // sema assumes that "clangd::" is a typo of "clang::" and uses + // "clang::" as the specified scope in: + // namespace clang { clangd::X; } + // In this case, we use the "typo" specifier as extra scope instead + // of using the scope assumed by sema. + if (!Spelling || llvm::StringRef(SpecifiedNS).ends_with(*Spelling)) { + Result.ResolvedScope = std::move(SpecifiedNS); + } else { + Result.UnresolvedScope = std::move(*Spelling); + } } else { - Result.UnresolvedScope = std::move(*Spelling); + Result.ResolvedScope = printNamespaceScope(*cast(NSB)->getNamespace()); } - } else if (const auto *ANS = Nested->getAsNamespaceAlias()) { - Result.ResolvedScope = printNamespaceScope(*ANS->getNamespace()); } else { // We don't fix symbols in scopes that are not top-level e.g. class // members, as we don't collect includes for them. diff --git a/clang-tools-extra/clangd/ModulesBuilder.cpp b/clang-tools-extra/clangd/ModulesBuilder.cpp index 6658111d6c7b4..56d00c22674e5 100644 --- a/clang-tools-extra/clangd/ModulesBuilder.cpp +++ b/clang-tools-extra/clangd/ModulesBuilder.cpp @@ -219,8 +219,9 @@ bool IsModuleFileUpToDate(PathRef ModuleFilePath, IntrusiveRefCntPtr ModCache = createCrossProcessModuleCache(); PCHContainerOperations PCHOperations; + CodeGenOptions CodeGenOpts; ASTReader Reader(PP, *ModCache, /*ASTContext=*/nullptr, - PCHOperations.getRawReader(), {}); + PCHOperations.getRawReader(), CodeGenOpts, {}); // We don't need any listener here. By default it will use a validator // listener. diff --git a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp index 00c05ebdb5216..67fc451a6a1a1 100644 --- a/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp +++ b/clang-tools-extra/clangd/refactor/tweaks/AddUsing.cpp @@ -173,7 +173,8 @@ findInsertionPoint(const Tweak::Selection &Inputs, if (SM.isBeforeInTranslationUnit(Inputs.Cursor, U->getUsingLoc())) // "Usings" is sorted, so we're done. break; - if (const auto *Namespace = U->getQualifier()->getAsNamespace()) { + if (const auto *Namespace = dyn_cast_if_present( + U->getQualifier()->getAsNamespace())) { if (Namespace->getCanonicalDecl() == QualifierToRemove.getNestedNameSpecifier() ->getAsNamespace() @@ -232,7 +233,10 @@ findInsertionPoint(const Tweak::Selection &Inputs, bool isNamespaceForbidden(const Tweak::Selection &Inputs, const NestedNameSpecifier &Namespace) { - std::string NamespaceStr = printNamespaceScope(*Namespace.getAsNamespace()); + const auto *NS = dyn_cast(Namespace.getAsNamespace()); + if (!NS) + return true; + std::string NamespaceStr = printNamespaceScope(*NS); for (StringRef Banned : Config::current().Style.FullyQualifiedNamespaces) { StringRef PrefixMatch = NamespaceStr; diff --git a/clang-tools-extra/clangd/support/Markup.cpp b/clang-tools-extra/clangd/support/Markup.cpp index 63aff96b02056..a13083026f26b 100644 --- a/clang-tools-extra/clangd/support/Markup.cpp +++ b/clang-tools-extra/clangd/support/Markup.cpp @@ -6,12 +6,12 @@ // //===----------------------------------------------------------------------===// #include "support/Markup.h" +#include "clang/Basic/CharInfo.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" -#include "llvm/Support/Compiler.h" #include "llvm/Support/raw_ostream.h" #include #include @@ -64,8 +64,8 @@ bool looksLikeTag(llvm::StringRef Contents) { // It's always safe to escape punctuation, but want minimal escaping. // The strategy is to escape the first character of anything that might start // a markdown grammar construct. -bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, - bool StartsLine) { +bool needsLeadingEscapePlaintext(char C, llvm::StringRef Before, + llvm::StringRef After, bool StartsLine) { assert(Before.take_while(llvm::isSpace).empty()); auto RulerLength = [&]() -> /*Length*/ unsigned { if (!StartsLine || !Before.empty()) @@ -151,16 +151,94 @@ bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, } } -/// Escape a markdown text block. Ensures the punctuation will not introduce +/// \brief Tests whether \p C should be backslash-escaped in markdown. +/// +/// The MarkupContent LSP specification defines that `markdown` content needs to +/// follow GFM (GitHub Flavored Markdown) rules. And we can assume that markdown +/// is rendered on the client side. This means we do not need to escape any +/// markdown constructs. +/// The only exception is when the client does not support HTML rendering in +/// markdown. In that case, we need to escape HTML tags and HTML entities. +/// +/// **FIXME:** handle the case when the client does support HTML rendering in +/// markdown. For this, the LSP server needs to check the +/// [supportsHtml +/// capability](https://github.com/microsoft/language-server-protocol/issues/1344) +/// of the client. +/// +/// \param C The character to check. +/// \param After The string that follows \p C . +/// This is used to determine if \p C is part of a tag or an entity reference. +/// +/// \returns true if \p C should be escaped, false otherwise. +bool needsLeadingEscapeMarkdown(char C, llvm::StringRef After) { + switch (C) { + case '<': // HTML tag (or autolink, which we choose not to escape) + return looksLikeTag(After); + case '&': { // HTML entity reference + auto End = After.find(';'); + if (End == llvm::StringRef::npos) + return false; + llvm::StringRef Content = After.substr(0, End); + if (Content.consume_front("#")) { + if (Content.consume_front("x") || Content.consume_front("X")) + return llvm::all_of(Content, llvm::isHexDigit); + return llvm::all_of(Content, llvm::isDigit); + } + return llvm::all_of(Content, llvm::isAlpha); + } + default: + return false; + } +} + +bool needsLeadingEscape(char C, llvm::StringRef Before, llvm::StringRef After, + bool StartsLine, bool EscapeMarkdown) { + if (EscapeMarkdown) + return needsLeadingEscapePlaintext(C, Before, After, StartsLine); + return needsLeadingEscapeMarkdown(C, After); +} + +/// Escape a markdown text block. +/// If \p EscapeMarkdown is true it ensures the punctuation will not introduce /// any of the markdown constructs. -std::string renderText(llvm::StringRef Input, bool StartsLine) { +/// Else, markdown syntax is not escaped, only HTML tags and entities. +std::string renderText(llvm::StringRef Input, bool StartsLine, + bool EscapeMarkdown) { std::string R; - for (unsigned I = 0; I < Input.size(); ++I) { - if (needsLeadingEscape(Input[I], Input.substr(0, I), Input.substr(I + 1), - StartsLine)) - R.push_back('\\'); - R.push_back(Input[I]); + R.reserve(Input.size()); + + // split the input into lines, and escape each line separately. + llvm::StringRef Line, Rest; + + bool IsFirstLine = true; + + for (std::tie(Line, Rest) = Input.split('\n'); + !(Line.empty() && Rest.empty()); + std::tie(Line, Rest) = Rest.split('\n')) { + + bool StartsLineIntern = IsFirstLine ? StartsLine : true; + + // Ignore leading spaces for the escape logic, but preserve them in the + // output. + StringRef LeadingSpaces = Line.take_while(llvm::isSpace); + if (!LeadingSpaces.empty()) { + R.append(LeadingSpaces); + } + + for (unsigned I = LeadingSpaces.size(); I < Line.size(); ++I) { + if (needsLeadingEscape(Line[I], Line.substr(LeadingSpaces.size(), I), + Line.substr(I + 1), StartsLineIntern, + EscapeMarkdown)) + R.push_back('\\'); + R.push_back(Line[I]); + } + + IsFirstLine = false; + if (!Rest.empty()) + R.push_back('\n'); } + return R; } @@ -168,6 +246,7 @@ std::string renderText(llvm::StringRef Input, bool StartsLine) { /// is surrounded by backticks and the inner contents are properly escaped. std::string renderInlineBlock(llvm::StringRef Input) { std::string R; + R.reserve(Input.size()); // Double all backticks to make sure we don't close the inline block early. for (size_t From = 0; From < Input.size();) { size_t Next = Input.find("`", From); @@ -261,6 +340,9 @@ std::string renderBlocks(llvm::ArrayRef> Children, // https://github.com/microsoft/vscode/issues/88416 for details. class Ruler : public Block { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override { + renderMarkdown(OS); + } void renderMarkdown(llvm::raw_ostream &OS) const override { // Note that we need an extra new line before the ruler, otherwise we might // make previous block a title instead of introducing a ruler. @@ -275,6 +357,9 @@ class Ruler : public Block { class CodeBlock : public Block { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override { + renderMarkdown(OS); + } void renderMarkdown(llvm::raw_ostream &OS) const override { std::string Marker = getMarkerForCodeBlock(Contents); // No need to pad from previous blocks, as they should end with a new line. @@ -303,11 +388,13 @@ class CodeBlock : public Block { std::string indentLines(llvm::StringRef Input) { assert(!Input.ends_with("\n") && "Input should've been trimmed."); std::string IndentedR; - // We'll add 2 spaces after each new line. + // We'll add 2 spaces after each new line which is not followed by another new + // line. IndentedR.reserve(Input.size() + Input.count('\n') * 2); - for (char C : Input) { + for (size_t I = 0; I < Input.size(); ++I) { + char C = Input[I]; IndentedR += C; - if (C == '\n') + if (C == '\n' && (((I + 1) < Input.size()) && (Input[I + 1] != '\n'))) IndentedR.append(" "); } return IndentedR; @@ -316,17 +403,34 @@ std::string indentLines(llvm::StringRef Input) { class Heading : public Paragraph { public: Heading(size_t Level) : Level(Level) {} + + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override { + insertHeadingMarkers(OS); + Paragraph::renderEscapedMarkdown(OS); + } + void renderMarkdown(llvm::raw_ostream &OS) const override { - OS << std::string(Level, '#') << ' '; + insertHeadingMarkers(OS); Paragraph::renderMarkdown(OS); } private: size_t Level; + + void insertHeadingMarkers(llvm::raw_ostream &OS) const { + OS << std::string(Level, '#') << ' '; + } }; } // namespace +std::string Block::asEscapedMarkdown() const { + std::string R; + llvm::raw_string_ostream OS(R); + renderEscapedMarkdown(OS); + return llvm::StringRef(OS.str()).trim().str(); +} + std::string Block::asMarkdown() const { std::string R; llvm::raw_string_ostream OS(R); @@ -341,6 +445,35 @@ std::string Block::asPlainText() const { return llvm::StringRef(OS.str()).trim().str(); } +void Paragraph::renderEscapedMarkdown(llvm::raw_ostream &OS) const { + bool NeedsSpace = false; + bool HasChunks = false; + for (auto &C : Chunks) { + if (C.SpaceBefore || NeedsSpace) + OS << " "; + switch (C.Kind) { + case ChunkKind::PlainText: + OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/true); + break; + case ChunkKind::InlineCode: + OS << renderInlineBlock(C.Contents); + break; + case ChunkKind::Bold: + OS << renderText("**" + C.Contents + "**", !HasChunks, + /*EscapeMarkdown=*/true); + break; + case ChunkKind::Emphasized: + OS << renderText("*" + C.Contents + "*", !HasChunks, + /*EscapeMarkdown=*/true); + break; + } + HasChunks = true; + NeedsSpace = C.SpaceAfter; + } + // A paragraph in markdown is separated by a blank line. + OS << "\n\n"; +} + void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const { bool NeedsSpace = false; bool HasChunks = false; @@ -348,20 +481,26 @@ void Paragraph::renderMarkdown(llvm::raw_ostream &OS) const { if (C.SpaceBefore || NeedsSpace) OS << " "; switch (C.Kind) { - case Chunk::PlainText: - OS << renderText(C.Contents, !HasChunks); + case ChunkKind::PlainText: + OS << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false); break; - case Chunk::InlineCode: + case ChunkKind::InlineCode: OS << renderInlineBlock(C.Contents); break; + case ChunkKind::Bold: + OS << "**" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false) + << "**"; + break; + case ChunkKind::Emphasized: + OS << "*" << renderText(C.Contents, !HasChunks, /*EscapeMarkdown=*/false) + << "*"; + break; } HasChunks = true; NeedsSpace = C.SpaceAfter; } - // Paragraphs are translated into markdown lines, not markdown paragraphs. - // Therefore it only has a single linebreak afterwards. - // VSCode requires two spaces at the end of line to start a new one. - OS << " \n"; + // A paragraph in markdown is separated by a blank line. + OS << "\n\n"; } std::unique_ptr Paragraph::clone() const { @@ -370,8 +509,8 @@ std::unique_ptr Paragraph::clone() const { /// Choose a marker to delimit `Text` from a prioritized list of options. /// This is more readable than escaping for plain-text. -llvm::StringRef chooseMarker(llvm::ArrayRef Options, - llvm::StringRef Text) { +llvm::StringRef Paragraph::chooseMarker(llvm::ArrayRef Options, + llvm::StringRef Text) const { // Prefer a delimiter whose characters don't appear in the text. for (llvm::StringRef S : Options) if (Text.find_first_of(S) == llvm::StringRef::npos) @@ -379,31 +518,147 @@ llvm::StringRef chooseMarker(llvm::ArrayRef Options, return Options.front(); } +bool Paragraph::punctuationIndicatesLineBreak(llvm::StringRef Line) const { + constexpr llvm::StringLiteral Punctuation = R"txt(.:,;!?)txt"; + + Line = Line.rtrim(); + return !Line.empty() && Punctuation.contains(Line.back()); +} + +bool Paragraph::isHardLineBreakIndicator(llvm::StringRef Rest) const { + // '-'/'*' md list, '@'/'\' documentation command, '>' md blockquote, + // '#' headings, '`' code blocks, two spaces (markdown force newline) + constexpr llvm::StringLiteral LinebreakIndicators = R"txt(-*@\>#`)txt"; + + Rest = Rest.ltrim(" \t"); + if (Rest.empty()) + return false; + + if (LinebreakIndicators.contains(Rest.front())) + return true; + + if (llvm::isDigit(Rest.front())) { + llvm::StringRef AfterDigit = Rest.drop_while(llvm::isDigit); + if (AfterDigit.starts_with(".") || AfterDigit.starts_with(")")) + return true; + } + return false; +} + +bool Paragraph::isHardLineBreakAfter(llvm::StringRef Line, + llvm::StringRef Rest) const { + // In Markdown, 2 spaces before a line break forces a line break. + // Add a line break for plaintext in this case too. + // Should we also consider whether Line is short? + return Line.ends_with(" ") || punctuationIndicatesLineBreak(Line) || + isHardLineBreakIndicator(Rest); +} + void Paragraph::renderPlainText(llvm::raw_ostream &OS) const { bool NeedsSpace = false; + std::string ConcatenatedText; + ConcatenatedText.reserve(EstimatedStringSize); + + llvm::raw_string_ostream ConcatenatedOS(ConcatenatedText); + for (auto &C : Chunks) { + + if (C.Kind == ChunkKind::PlainText) { + if (C.SpaceBefore || NeedsSpace) + ConcatenatedOS << ' '; + + ConcatenatedOS << C.Contents; + NeedsSpace = llvm::isSpace(C.Contents.back()) || C.SpaceAfter; + continue; + } + if (C.SpaceBefore || NeedsSpace) - OS << " "; + ConcatenatedOS << ' '; llvm::StringRef Marker = ""; - if (C.Preserve && C.Kind == Chunk::InlineCode) + if (C.Preserve && C.Kind == ChunkKind::InlineCode) Marker = chooseMarker({"`", "'", "\""}, C.Contents); - OS << Marker << C.Contents << Marker; + else if (C.Kind == ChunkKind::Bold) + Marker = "**"; + else if (C.Kind == ChunkKind::Emphasized) + Marker = "*"; + ConcatenatedOS << Marker << C.Contents << Marker; NeedsSpace = C.SpaceAfter; } - OS << '\n'; + + // We go through the contents line by line to handle the newlines + // and required spacing correctly. + // + // Newlines are added if: + // - the line ends with 2 spaces and a newline follows + // - the line ends with punctuation that indicates a line break (.:,;!?) + // - the next line starts with a hard line break indicator (-@>#`, or a digit + // followed by '.' or ')'), ignoring leading whitespace. + // + // Otherwise, newlines in the input are replaced with a single space. + // + // Multiple spaces are collapsed into a single space. + // + // Lines containing only whitespace are ignored. + llvm::StringRef Line, Rest; + + for (std::tie(Line, Rest) = + llvm::StringRef(ConcatenatedText).trim().split('\n'); + !(Line.empty() && Rest.empty()); + std::tie(Line, Rest) = Rest.split('\n')) { + + // Remove lines which only contain whitespace. + // + // Note: this also handles the case when there are multiple newlines + // in a row, since all leading newlines are removed. + // + // The documentation parsing treats multiple newlines as paragraph + // separators, hence it will create a new Paragraph instead of adding + // multiple newlines to the same Paragraph. + // Therfore multiple newlines are never added to a paragraph + // except if the user explicitly adds them using + // e.g. appendText("user text\n\nnext text"). + Line = Line.ltrim(); + if (Line.empty()) + continue; + + OS << canonicalizeSpaces(Line); + + if (isHardLineBreakAfter(Line, Rest)) + OS << '\n'; + else if (!Rest.empty()) + // Since we removed any trailing whitespace from the input using trim(), + // we know that the next line contains non-whitespace characters. + // Therefore, we can add a space without worrying about trailing spaces. + OS << ' '; + } + + // Paragraphs are separated by a blank line. + OS << "\n\n"; } BulletList::BulletList() = default; BulletList::~BulletList() = default; +void BulletList::renderEscapedMarkdown(llvm::raw_ostream &OS) const { + for (auto &D : Items) { + std::string M = D.asEscapedMarkdown(); + // Instead of doing this we might prefer passing Indent to children to get + // rid of the copies, if it turns out to be a bottleneck. + OS << "- " << indentLines(M) << '\n'; + } + // We add 2 newlines after list to terminate it in markdown. + OS << "\n\n"; +} + void BulletList::renderMarkdown(llvm::raw_ostream &OS) const { for (auto &D : Items) { + std::string M = D.asMarkdown(); // Instead of doing this we might prefer passing Indent to children to get // rid of the copies, if it turns out to be a bottleneck. - OS << "- " << indentLines(D.asMarkdown()) << '\n'; + OS << "- " << indentLines(M) << '\n'; } - // We need a new line after list to terminate it in markdown. - OS << '\n'; + // We add 2 newlines after list to terminate it in markdown. + OS << "\n\n"; } void BulletList::renderPlainText(llvm::raw_ostream &OS) const { @@ -412,6 +667,7 @@ void BulletList::renderPlainText(llvm::raw_ostream &OS) const { // rid of the copies, if it turns out to be a bottleneck. OS << "- " << indentLines(D.asPlainText()) << '\n'; } + OS << '\n'; } Paragraph &Paragraph::appendSpace() { @@ -420,32 +676,51 @@ Paragraph &Paragraph::appendSpace() { return *this; } -Paragraph &Paragraph::appendText(llvm::StringRef Text) { - std::string Norm = canonicalizeSpaces(Text); - if (Norm.empty()) +Paragraph &Paragraph::appendChunk(llvm::StringRef Contents, ChunkKind K) { + if (Contents.empty()) return *this; Chunks.emplace_back(); Chunk &C = Chunks.back(); - C.Contents = std::move(Norm); - C.Kind = Chunk::PlainText; - C.SpaceBefore = llvm::isSpace(Text.front()); - C.SpaceAfter = llvm::isSpace(Text.back()); + C.Contents = Contents; + C.Kind = K; + + EstimatedStringSize += Contents.size(); return *this; } +Paragraph &Paragraph::appendText(llvm::StringRef Text) { + if (!Chunks.empty() && Chunks.back().Kind == ChunkKind::PlainText) { + Chunks.back().Contents += std::move(Text); + return *this; + } + + return appendChunk(std::move(Text), ChunkKind::PlainText); +} + +Paragraph &Paragraph::appendEmphasizedText(llvm::StringRef Text) { + return appendChunk(canonicalizeSpaces(std::move(Text)), + ChunkKind::Emphasized); +} + +Paragraph &Paragraph::appendBoldText(llvm::StringRef Text) { + return appendChunk(canonicalizeSpaces(std::move(Text)), ChunkKind::Bold); +} + Paragraph &Paragraph::appendCode(llvm::StringRef Code, bool Preserve) { bool AdjacentCode = - !Chunks.empty() && Chunks.back().Kind == Chunk::InlineCode; + !Chunks.empty() && Chunks.back().Kind == ChunkKind::InlineCode; std::string Norm = canonicalizeSpaces(std::move(Code)); if (Norm.empty()) return *this; + EstimatedStringSize += Norm.size(); Chunks.emplace_back(); Chunk &C = Chunks.back(); C.Contents = std::move(Norm); - C.Kind = Chunk::InlineCode; + C.Kind = ChunkKind::InlineCode; C.Preserve = Preserve; // Disallow adjacent code spans without spaces, markdown can't render them. C.SpaceBefore = AdjacentCode; + return *this; } @@ -482,6 +757,10 @@ void Document::addCodeBlock(std::string Code, std::string Language) { std::make_unique(std::move(Code), std::move(Language))); } +std::string Document::asEscapedMarkdown() const { + return renderBlocks(Children, &Block::renderEscapedMarkdown); +} + std::string Document::asMarkdown() const { return renderBlocks(Children, &Block::renderMarkdown); } diff --git a/clang-tools-extra/clangd/support/Markup.h b/clang-tools-extra/clangd/support/Markup.h index 3a869c49a2cbb..eea6328f69a12 100644 --- a/clang-tools-extra/clangd/support/Markup.h +++ b/clang-tools-extra/clangd/support/Markup.h @@ -27,9 +27,11 @@ namespace markup { /// should trim them if need be. class Block { public: + virtual void renderEscapedMarkdown(llvm::raw_ostream &OS) const = 0; virtual void renderMarkdown(llvm::raw_ostream &OS) const = 0; virtual void renderPlainText(llvm::raw_ostream &OS) const = 0; virtual std::unique_ptr clone() const = 0; + std::string asEscapedMarkdown() const; std::string asMarkdown() const; std::string asPlainText() const; @@ -42,6 +44,7 @@ class Block { /// One must introduce different paragraphs to create separate blocks. class Paragraph : public Block { public: + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override; void renderMarkdown(llvm::raw_ostream &OS) const override; void renderPlainText(llvm::raw_ostream &OS) const override; std::unique_ptr clone() const override; @@ -49,6 +52,12 @@ class Paragraph : public Block { /// Append plain text to the end of the string. Paragraph &appendText(llvm::StringRef Text); + /// Append emphasized text, this translates to the * block in markdown. + Paragraph &appendEmphasizedText(llvm::StringRef Text); + + /// Append bold text, this translates to the ** block in markdown. + Paragraph &appendBoldText(llvm::StringRef Text); + /// Append inline code, this translates to the ` block in markdown. /// \p Preserve indicates the code span must be apparent even in plaintext. Paragraph &appendCode(llvm::StringRef Code, bool Preserve = false); @@ -58,11 +67,9 @@ class Paragraph : public Block { Paragraph &appendSpace(); private: + enum ChunkKind { PlainText, InlineCode, Bold, Emphasized }; struct Chunk { - enum { - PlainText, - InlineCode, - } Kind = PlainText; + ChunkKind Kind = PlainText; // Preserve chunk markers in plaintext. bool Preserve = false; std::string Contents; @@ -73,6 +80,21 @@ class Paragraph : public Block { bool SpaceAfter = false; }; std::vector Chunks; + + /// Estimated size of the string representation of this paragraph. + /// Used to reserve space in the output string. + /// Each time paragraph content is added, this value is updated. + /// This is an estimate, so it may not be accurate but can help + /// reducing dynamically reallocating string memory. + unsigned EstimatedStringSize = 0; + + Paragraph &appendChunk(llvm::StringRef Contents, ChunkKind K); + + llvm::StringRef chooseMarker(llvm::ArrayRef Options, + llvm::StringRef Text) const; + bool punctuationIndicatesLineBreak(llvm::StringRef Line) const; + bool isHardLineBreakIndicator(llvm::StringRef Rest) const; + bool isHardLineBreakAfter(llvm::StringRef Line, llvm::StringRef Rest) const; }; /// Represents a sequence of one or more documents. Knows how to print them in a @@ -82,6 +104,10 @@ class BulletList : public Block { BulletList(); ~BulletList(); + // A BulletList rendered in markdown is a tight list if it is not a nested + // list and no item contains multiple paragraphs. Otherwise, it is a loose + // list. + void renderEscapedMarkdown(llvm::raw_ostream &OS) const override; void renderMarkdown(llvm::raw_ostream &OS) const override; void renderPlainText(llvm::raw_ostream &OS) const override; std::unique_ptr clone() const override; @@ -117,9 +143,13 @@ class Document { BulletList &addBulletList(); + /// Doesn't contain any trailing newlines and escaped markdown syntax. + /// It is expected that the result of this function + /// is rendered as markdown. + std::string asEscapedMarkdown() const; /// Doesn't contain any trailing newlines. - /// We try to make the markdown human-readable, e.g. avoid extra escaping. - /// At least one client (coc.nvim) displays the markdown verbatim! + /// It is expected that the result of this function + /// is rendered as markdown. std::string asMarkdown() const; /// Doesn't contain any trailing newlines. std::string asPlainText() const; diff --git a/clang-tools-extra/clangd/test/signature-help-unescaped.test b/clang-tools-extra/clangd/test/signature-help-unescaped.test new file mode 100644 index 0000000000000..c8a7df81580c9 --- /dev/null +++ b/clang-tools-extra/clangd/test/signature-help-unescaped.test @@ -0,0 +1,40 @@ +# We specify a custom path in XDG_CONFIG_HOME, which only works on some systems. +# UNSUPPORTED: system-windows +# UNSUPPORTED: system-darwin + +# RUN: mkdir -p %t/clangd + +# Create a config file that configures to use CommentFormat Markdown. +# RUN: echo 'Documentation:' > %t/clangd/config.yaml +# RUN: echo ' CommentFormat: Markdown' >> %t/clangd/config.yaml +# RUN: env XDG_CONFIG_HOME=%t clangd -lit-test -enable-config < %s | FileCheck -strict-whitespace %s +# Start a session. +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{"textDocument": {"signatureHelp": {"signatureInformation": {"documentationFormat": ["markdown", "plaintext"]}}}},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///main.cpp","languageId":"cpp","version":1,"text":"// comment `markdown` _noescape_\nvoid x(int);\nint main(){\nx("}}} +--- +{"jsonrpc":"2.0","id":1,"method":"textDocument/signatureHelp","params":{"textDocument":{"uri":"test:///main.cpp"},"position":{"line":3,"character":2}}} +# CHECK: "id": 1, +# CHECK-NEXT: "jsonrpc": "2.0", +# CHECK-NEXT: "result": { +# CHECK-NEXT: "activeParameter": 0, +# CHECK-NEXT: "activeSignature": 0, +# CHECK-NEXT: "signatures": [ +# CHECK-NEXT: { +# CHECK-NEXT: "documentation": { +# CHECK-NEXT: "kind": "markdown", +# CHECK-NEXT: "value": "comment `markdown` _noescape_" +# CHECK-NEXT: }, +# CHECK-NEXT: "label": "x(int) -> void", +# CHECK-NEXT: "parameters": [ +# CHECK-NEXT: { +# CHECK-NEXT: "label": "int" +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +# CHECK-NEXT: ] +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":100000,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"} diff --git a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp index b7c64c7a06745..5a5d815076e2a 100644 --- a/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp +++ b/clang-tools-extra/clangd/unittests/CodeCompleteTests.cpp @@ -1095,10 +1095,11 @@ TEST(CompletionTest, Documentation) { int x = ^ )cpp"); - EXPECT_THAT(Results.Completions, - Contains(AllOf( - named("foo"), - doc("Annotation: custom_annotation\nNon-doxygen comment.")))); + EXPECT_THAT( + Results.Completions, + Contains( + AllOf(named("foo"), + doc("Annotation: custom_annotation\n\nNon-doxygen comment.")))); EXPECT_THAT( Results.Completions, Contains(AllOf(named("bar"), doc("Doxygen comment.\n\\param int a")))); @@ -2297,7 +2298,7 @@ TEST(CompletionTest, Render) { EXPECT_EQ(R.insertTextFormat, InsertTextFormat::PlainText); EXPECT_EQ(R.filterText, "x"); EXPECT_EQ(R.detail, "int"); - EXPECT_EQ(R.documentation->value, "From \"foo.h\"\nThis is x()"); + EXPECT_EQ(R.documentation->value, "From \"foo.h\"\n\nThis is x()"); EXPECT_THAT(R.additionalTextEdits, IsEmpty()); EXPECT_EQ(R.sortText, sortText(1.0, "x")); EXPECT_FALSE(R.deprecated); @@ -2332,7 +2333,7 @@ TEST(CompletionTest, Render) { C.BundleSize = 2; R = C.render(Opts); EXPECT_EQ(R.detail, "[2 overloads]"); - EXPECT_EQ(R.documentation->value, "From \"foo.h\"\nThis is x()"); + EXPECT_EQ(R.documentation->value, "From \"foo.h\"\n\nThis is x()"); C.Deprecated = true; R = C.render(Opts); @@ -2340,7 +2341,7 @@ TEST(CompletionTest, Render) { Opts.DocumentationFormat = MarkupKind::Markdown; R = C.render(Opts); - EXPECT_EQ(R.documentation->value, "From `\"foo.h\"` \nThis is `x()`"); + EXPECT_EQ(R.documentation->value, "From `\"foo.h\"`\n\nThis is `x()`"); } TEST(CompletionTest, IgnoreRecoveryResults) { diff --git a/clang-tools-extra/clangd/unittests/FindTargetTests.cpp b/clang-tools-extra/clangd/unittests/FindTargetTests.cpp index 602f61d9ecb41..4d77f9d690ca0 100644 --- a/clang-tools-extra/clangd/unittests/FindTargetTests.cpp +++ b/clang-tools-extra/clangd/unittests/FindTargetTests.cpp @@ -838,7 +838,7 @@ TEST_F(TargetDeclTest, OverloadExpr) { )cpp"; // Sized deallocation is enabled by default in C++14 onwards. EXPECT_DECLS("CXXDeleteExpr", - "void operator delete(void *, unsigned long) noexcept"); + "void operator delete(void *, __size_t) noexcept"); } TEST_F(TargetDeclTest, DependentExprs) { diff --git a/clang-tools-extra/clangd/unittests/HoverTests.cpp b/clang-tools-extra/clangd/unittests/HoverTests.cpp index 775278ccf694b..12d260db7ea11 100644 --- a/clang-tools-extra/clangd/unittests/HoverTests.cpp +++ b/clang-tools-extra/clangd/unittests/HoverTests.cpp @@ -10,6 +10,7 @@ #include "Annotations.h" #include "Config.h" #include "Hover.h" +#include "Protocol.h" #include "TestFS.h" #include "TestIndex.h" #include "TestTU.h" @@ -2794,7 +2795,7 @@ TEST(Hover, All) { })cpp", [](HoverInfo &HI) { HI.Name = "expression"; - HI.Type = "unsigned long"; + HI.Type = {"__size_t", "unsigned long"}; HI.Value = "1"; }}, { @@ -2804,7 +2805,7 @@ TEST(Hover, All) { })cpp", [](HoverInfo &HI) { HI.Name = "expression"; - HI.Type = "unsigned long"; + HI.Type = {"__size_t", "unsigned long"}; HI.Value = "1"; }}, { @@ -3125,7 +3126,7 @@ TEST(Hover, All) { Expected.SymRange = T.range(); Case.ExpectedBuilder(Expected); - SCOPED_TRACE(H->present().asPlainText()); + SCOPED_TRACE(H->present(MarkupKind::PlainText)); EXPECT_EQ(H->NamespaceScope, Expected.NamespaceScope); EXPECT_EQ(H->LocalScope, Expected.LocalScope); EXPECT_EQ(H->Name, Expected.Name); @@ -3217,7 +3218,7 @@ TEST(Hover, Providers) { ASSERT_TRUE(H); HoverInfo Expected; Case.ExpectedBuilder(Expected); - SCOPED_TRACE(H->present().asMarkdown()); + SCOPED_TRACE(H->present(MarkupKind::Markdown)); EXPECT_EQ(H->Provider, Expected.Provider); } } @@ -3233,11 +3234,11 @@ TEST(Hover, ParseProviderInfo) { struct Case { HoverInfo HI; llvm::StringRef ExpectedMarkdown; - } Cases[] = {{HIFoo, "### `foo` \nprovided by `\"foo.h\"`"}, - {HIFooBar, "### `foo` \nprovided by ``"}}; + } Cases[] = {{HIFoo, "### `foo`\n\nprovided by `\"foo.h\"`"}, + {HIFooBar, "### `foo`\n\nprovided by ``"}}; for (const auto &Case : Cases) - EXPECT_EQ(Case.HI.present().asMarkdown(), Case.ExpectedMarkdown); + EXPECT_EQ(Case.HI.present(MarkupKind::Markdown), Case.ExpectedMarkdown); } TEST(Hover, UsedSymbols) { @@ -3287,7 +3288,7 @@ TEST(Hover, UsedSymbols) { ASSERT_TRUE(H); HoverInfo Expected; Case.ExpectedBuilder(Expected); - SCOPED_TRACE(H->present().asMarkdown()); + SCOPED_TRACE(H->present(MarkupKind::Markdown)); EXPECT_EQ(H->UsedSymbolNames, Expected.UsedSymbolNames); } } @@ -3441,6 +3442,7 @@ TEST(Hover, Present) { R"(class foo Size: 10 bytes + documentation template class Foo {})", @@ -3465,8 +3467,8 @@ template class Foo {})", }, "function foo\n" "\n" - "→ ret_type (aka can_ret_type)\n" - "Parameters:\n" + "→ ret_type (aka can_ret_type)\n\n" + "Parameters:\n\n" "- \n" "- type (aka can_type)\n" "- type foo (aka can_type)\n" @@ -3491,8 +3493,11 @@ template class Foo {})", R"(field foo Type: type (aka can_type) + Value = value + Offset: 12 bytes + Size: 4 bytes (+4 bytes padding), alignment 4 bytes // In test::Bar @@ -3514,8 +3519,11 @@ def)", R"(field foo Type: type (aka can_type) + Value = value + Offset: 4 bytes and 3 bits + Size: 25 bits (+4 bits padding), alignment 8 bytes // In test::Bar @@ -3573,6 +3581,7 @@ protected: size_t method())", R"(constructor cls Parameters: + - int a - int b = 5 @@ -3609,7 +3618,9 @@ private: union foo {})", R"(variable foo Type: int + Value = 3 + Passed as arg_a // In test::Bar @@ -3644,7 +3655,9 @@ Passed by value)", R"(variable foo Type: int + Value = 3 + Passed by reference as arg_a // In test::Bar @@ -3667,7 +3680,9 @@ int foo = 3)", R"(variable foo Type: int + Value = 3 + Passed as arg_a (converted to alias_int) // In test::Bar @@ -3705,7 +3720,9 @@ int foo = 3)", R"(variable foo Type: int + Value = 3 + Passed by const reference as arg_a (converted to int) // In test::Bar @@ -3741,83 +3758,140 @@ provides Foo, Bar, Baz, Foobar, Qux and 1 more)"}}; Config Cfg; Cfg.Hover.ShowAKA = true; WithContextValue WithCfg(Config::Key, std::move(Cfg)); - EXPECT_EQ(HI.present().asPlainText(), C.ExpectedRender); + EXPECT_EQ(HI.present(MarkupKind::PlainText), C.ExpectedRender); } } TEST(Hover, ParseDocumentation) { struct Case { llvm::StringRef Documentation; + llvm::StringRef ExpectedRenderEscapedMarkdown; llvm::StringRef ExpectedRenderMarkdown; llvm::StringRef ExpectedRenderPlainText; } Cases[] = {{ " \n foo\nbar", - "foo bar", + "foo\nbar", + "foo\nbar", "foo bar", }, { "foo\nbar \n ", - "foo bar", + "foo\nbar", + "foo\nbar", "foo bar", }, { "foo \nbar", - "foo bar", - "foo bar", + "foo \nbar", + "foo \nbar", + "foo\nbar", }, { "foo \nbar", - "foo bar", - "foo bar", + "foo \nbar", + "foo \nbar", + "foo\nbar", }, { "foo\n\n\nbar", - "foo \nbar", - "foo\nbar", + "foo\n\nbar", + "foo\n\nbar", + "foo\n\nbar", }, { "foo\n\n\n\tbar", - "foo \nbar", - "foo\nbar", + "foo\n\n\tbar", + "foo\n\n\tbar", + "foo\n\nbar", + }, + { + "foo\n\n\n bar", + "foo\n\n bar", + "foo\n\n bar", + "foo\n\nbar", + }, + { + "foo\n\n\n bar", + "foo\n\n bar", + "foo\n\n bar", + "foo\n\nbar", }, { "foo\n\n\n bar", - "foo \nbar", - "foo\nbar", + "foo\n\n bar", + "foo\n\n bar", + "foo\n\nbar", + }, + { + "foo\n\n\n\nbar", + "foo\n\nbar", + "foo\n\nbar", + "foo\n\nbar", + }, + { + "foo\n\n\n\n\tbar", + "foo\n\n\tbar", + "foo\n\n\tbar", + "foo\n\nbar", + }, + { + "foo\n\n\n\n bar", + "foo\n\n bar", + "foo\n\n bar", + "foo\n\nbar", + }, + { + "foo\n\n\n\n bar", + "foo\n\n bar", + "foo\n\n bar", + "foo\n\nbar", + }, + { + "foo\n\n\n\n bar", + "foo\n\n bar", + "foo\n\n bar", + "foo\n\nbar", }, { "foo.\nbar", - "foo. \nbar", + "foo.\nbar", + "foo.\nbar", "foo.\nbar", }, { "foo. \nbar", - "foo. \nbar", + "foo. \nbar", + "foo. \nbar", "foo.\nbar", }, { "foo\n*bar", - "foo \n\\*bar", + "foo\n\\*bar", + "foo\n*bar", "foo\n*bar", }, { "foo\nbar", - "foo bar", + "foo\nbar", + "foo\nbar", "foo bar", }, { "Tests primality of `p`.", "Tests primality of `p`.", "Tests primality of `p`.", + "Tests primality of `p`.", }, { "'`' should not occur in `Code`", "'\\`' should not occur in `Code`", "'`' should not occur in `Code`", + "'`' should not occur in `Code`", }, { "`not\nparsed`", - "\\`not parsed\\`", + "\\`not\nparsed\\`", + "`not\nparsed`", "`not parsed`", }}; @@ -3825,6 +3899,7 @@ TEST(Hover, ParseDocumentation) { markup::Document Output; parseDocumentation(C.Documentation, Output); + EXPECT_EQ(Output.asEscapedMarkdown(), C.ExpectedRenderEscapedMarkdown); EXPECT_EQ(Output.asMarkdown(), C.ExpectedRenderMarkdown); EXPECT_EQ(Output.asPlainText(), C.ExpectedRenderPlainText); } @@ -3837,7 +3912,7 @@ TEST(Hover, PresentHeadings) { HI.Kind = index::SymbolKind::Variable; HI.Name = "foo"; - EXPECT_EQ(HI.present().asMarkdown(), "### variable `foo`"); + EXPECT_EQ(HI.present(MarkupKind::Markdown), "### variable `foo`"); } // This is a separate test as rulers behave differently in markdown vs @@ -3850,23 +3925,23 @@ TEST(Hover, PresentRulers) { HI.Definition = "def"; llvm::StringRef ExpectedMarkdown = // - "### variable `foo` \n" + "### variable `foo`\n" "\n" "---\n" - "Value = `val` \n" + "Value = `val`\n" "\n" "---\n" "```cpp\n" "def\n" "```"; - EXPECT_EQ(HI.present().asMarkdown(), ExpectedMarkdown); + EXPECT_EQ(HI.present(MarkupKind::Markdown), ExpectedMarkdown); llvm::StringRef ExpectedPlaintext = R"pt(variable foo Value = val def)pt"; - EXPECT_EQ(HI.present().asPlainText(), ExpectedPlaintext); + EXPECT_EQ(HI.present(MarkupKind::PlainText), ExpectedPlaintext); } TEST(Hover, SpaceshipTemplateNoCrash) { diff --git a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp index 2d86c91c7ec08..482f230fb86fe 100644 --- a/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp +++ b/clang-tools-extra/clangd/unittests/support/MarkupTests.cpp @@ -17,6 +17,10 @@ namespace markup { namespace { std::string escape(llvm::StringRef Text) { + return Paragraph().appendText(Text.str()).asEscapedMarkdown(); +} + +std::string dontEscape(llvm::StringRef Text) { return Paragraph().appendText(Text.str()).asMarkdown(); } @@ -107,15 +111,132 @@ TEST(Render, Escaping) { // In code blocks we don't need to escape ASCII punctuation. Paragraph P = Paragraph(); P.appendCode("* foo !+ bar * baz"); - EXPECT_EQ(P.asMarkdown(), "`* foo !+ bar * baz`"); + EXPECT_EQ(P.asEscapedMarkdown(), "`* foo !+ bar * baz`"); // But we have to escape the backticks. P = Paragraph(); P.appendCode("foo`bar`baz", /*Preserve=*/true); - EXPECT_EQ(P.asMarkdown(), "`foo``bar``baz`"); + EXPECT_EQ(P.asEscapedMarkdown(), "`foo``bar``baz`"); // In plain-text, we fall back to different quotes. EXPECT_EQ(P.asPlainText(), "'foo`bar`baz'"); + // Inline code blocks starting or ending with backticks should add spaces. + P = Paragraph(); + P.appendCode("`foo"); + EXPECT_EQ(P.asEscapedMarkdown(), "` ``foo `"); + P = Paragraph(); + P.appendCode("foo`"); + EXPECT_EQ(P.asEscapedMarkdown(), "` foo`` `"); + P = Paragraph(); + P.appendCode("`foo`"); + EXPECT_EQ(P.asEscapedMarkdown(), "` ``foo`` `"); + + // Code blocks might need more than 3 backticks. + Document D; + D.addCodeBlock("foobarbaz `\nqux"); + EXPECT_EQ(D.asEscapedMarkdown(), "```cpp\n" + "foobarbaz `\nqux\n" + "```"); + D = Document(); + D.addCodeBlock("foobarbaz ``\nqux"); + EXPECT_THAT(D.asEscapedMarkdown(), "```cpp\n" + "foobarbaz ``\nqux\n" + "```"); + D = Document(); + D.addCodeBlock("foobarbaz ```\nqux"); + EXPECT_EQ(D.asEscapedMarkdown(), "````cpp\n" + "foobarbaz ```\nqux\n" + "````"); + D = Document(); + D.addCodeBlock("foobarbaz ` `` ``` ```` `\nqux"); + EXPECT_EQ(D.asEscapedMarkdown(), "`````cpp\n" + "foobarbaz ` `` ``` ```` `\nqux\n" + "`````"); +} + +TEST(Render, NoEscaping) { + // Check all ASCII punctuation. + std::string Punctuation = R"txt(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)txt"; + EXPECT_EQ(dontEscape(Punctuation), Punctuation); + + // Inline code + EXPECT_THAT(dontEscape("`foo`"), escapedNone()); + EXPECT_THAT(dontEscape("`foo"), escapedNone()); + EXPECT_THAT(dontEscape("foo`"), escapedNone()); + EXPECT_THAT(dontEscape("``foo``"), escapedNone()); + // Code blocks + EXPECT_THAT(dontEscape("```"), escapedNone()); + EXPECT_THAT(dontEscape("~~~"), escapedNone()); + + // Rulers and headings + EXPECT_THAT(dontEscape("## Heading"), escapedNone()); + EXPECT_THAT(dontEscape("Foo # bar"), escapedNone()); + EXPECT_THAT(dontEscape("---"), escapedNone()); + EXPECT_THAT(dontEscape("-"), escapedNone()); + EXPECT_THAT(dontEscape("==="), escapedNone()); + EXPECT_THAT(dontEscape("="), escapedNone()); + EXPECT_THAT(dontEscape("***"), escapedNone()); // \** could start emphasis! + + // HTML tags. + EXPECT_THAT(dontEscape(""), escaped('<')); + EXPECT_THAT(dontEscape("std::vector"), escaped('<')); + EXPECT_THAT(dontEscape("std::map"), escapedNone()); + // Autolinks + EXPECT_THAT(dontEscape("Email "), escapedNone()); + EXPECT_THAT(dontEscape("Website "), escapedNone()); + + // Bullet lists. + EXPECT_THAT(dontEscape("- foo"), escapedNone()); + EXPECT_THAT(dontEscape("* foo"), escapedNone()); + EXPECT_THAT(dontEscape("+ foo"), escapedNone()); + EXPECT_THAT(dontEscape("+"), escapedNone()); + EXPECT_THAT(dontEscape("a + foo"), escapedNone()); + EXPECT_THAT(dontEscape("a+ foo"), escapedNone()); + EXPECT_THAT(dontEscape("1. foo"), escapedNone()); + EXPECT_THAT(dontEscape("a. foo"), escapedNone()); + + // Emphasis. + EXPECT_THAT(dontEscape("*foo*"), escapedNone()); + EXPECT_THAT(dontEscape("**foo**"), escapedNone()); + EXPECT_THAT(dontEscape("*foo"), escapedNone()); + EXPECT_THAT(dontEscape("foo *"), escapedNone()); + EXPECT_THAT(dontEscape("foo * bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo_bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo _bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo_ bar"), escapedNone()); + EXPECT_THAT(dontEscape("foo _ bar"), escapedNone()); + + // HTML entities. + EXPECT_THAT(dontEscape("fish &chips;"), escaped('&')); + EXPECT_THAT(dontEscape("fish & chips;"), escapedNone()); + EXPECT_THAT(dontEscape("fish &chips"), escapedNone()); + EXPECT_THAT(dontEscape("foo * bar"), escaped('&')); + EXPECT_THAT(dontEscape("foo ¯ bar"), escaped('&')); + EXPECT_THAT(dontEscape("foo &?; bar"), escapedNone()); + + // Links. + EXPECT_THAT(dontEscape("[foo](bar)"), escapedNone()); + EXPECT_THAT(dontEscape("[foo]: bar"), escapedNone()); + // No need to escape these, as the target never exists. + EXPECT_THAT(dontEscape("[foo][]"), escapedNone()); + EXPECT_THAT(dontEscape("[foo][bar]"), escapedNone()); + EXPECT_THAT(dontEscape("[foo]"), escapedNone()); + + // In code blocks we don't need to escape ASCII punctuation. + Paragraph P = Paragraph(); + P.appendCode("* foo !+ bar * baz"); + EXPECT_EQ(P.asMarkdown(), "`* foo !+ bar * baz`"); + + // But we have to escape the backticks. + P = Paragraph(); + P.appendCode("foo`bar`baz", /*Preserve=*/true); + EXPECT_EQ(P.asMarkdown(), "`foo``bar``baz`"); + // Inline code blocks starting or ending with backticks should add spaces. P = Paragraph(); P.appendCode("`foo"); @@ -150,17 +271,6 @@ TEST(Render, Escaping) { "`````"); } -TEST(Paragraph, Chunks) { - Paragraph P = Paragraph(); - P.appendText("One "); - P.appendCode("fish"); - P.appendText(", two "); - P.appendCode("fish", /*Preserve=*/true); - - EXPECT_EQ(P.asMarkdown(), "One `fish`, two `fish`"); - EXPECT_EQ(P.asPlainText(), "One fish, two `fish`"); -} - TEST(Paragraph, SeparationOfChunks) { // This test keeps appending contents to a single Paragraph and checks // expected accumulated contents after each one. @@ -168,28 +278,122 @@ TEST(Paragraph, SeparationOfChunks) { Paragraph P; P.appendText("after "); + EXPECT_EQ(P.asEscapedMarkdown(), "after"); EXPECT_EQ(P.asMarkdown(), "after"); EXPECT_EQ(P.asPlainText(), "after"); P.appendCode("foobar").appendSpace(); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar`"); EXPECT_EQ(P.asMarkdown(), "after `foobar`"); EXPECT_EQ(P.asPlainText(), "after foobar"); P.appendText("bat"); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar` bat"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat"); EXPECT_EQ(P.asPlainText(), "after foobar bat"); P.appendCode("no").appendCode("space"); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar` bat`no` `space`"); EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space`"); EXPECT_EQ(P.asPlainText(), "after foobar batno space"); + + P.appendText(" text"); + EXPECT_EQ(P.asEscapedMarkdown(), "after `foobar` bat`no` `space` text"); + EXPECT_EQ(P.asMarkdown(), "after `foobar` bat`no` `space` text"); + EXPECT_EQ(P.asPlainText(), "after foobar batno space text"); + + P.appendSpace().appendCode("code").appendText(".\n newline"); + EXPECT_EQ(P.asEscapedMarkdown(), + "after `foobar` bat`no` `space` text `code`.\n newline"); + EXPECT_EQ(P.asMarkdown(), + "after `foobar` bat`no` `space` text `code`.\n newline"); + EXPECT_EQ(P.asPlainText(), "after foobar batno space text code.\nnewline"); +} + +TEST(Paragraph, SeparationOfChunks2) { + // This test keeps appending contents to a single Paragraph and checks + // expected accumulated contents after each one. + // Purpose is to check for separation between different chunks + // where the spacing is in the appended string rather set by appendSpace. + Paragraph P; + + P.appendText("after "); + EXPECT_EQ(P.asEscapedMarkdown(), "after"); + EXPECT_EQ(P.asMarkdown(), "after"); + EXPECT_EQ(P.asPlainText(), "after"); + + P.appendText("foobar"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar"); + EXPECT_EQ(P.asMarkdown(), "after foobar"); + EXPECT_EQ(P.asPlainText(), "after foobar"); + + P.appendText(" bat"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar bat"); + EXPECT_EQ(P.asMarkdown(), "after foobar bat"); + EXPECT_EQ(P.asPlainText(), "after foobar bat"); + + P.appendText("baz"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz"); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz"); + + P.appendText(" faz "); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz"); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz"); + + P.appendText(" bar "); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz bar"); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz bar"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz bar"); + + P.appendText("qux"); + EXPECT_EQ(P.asEscapedMarkdown(), "after foobar batbaz faz bar qux"); + EXPECT_EQ(P.asMarkdown(), "after foobar batbaz faz bar qux"); + EXPECT_EQ(P.asPlainText(), "after foobar batbaz faz bar qux"); +} + +TEST(Paragraph, SeparationOfChunks3) { + // This test keeps appending contents to a single Paragraph and checks + // expected accumulated contents after each one. + // Purpose is to check for separation between different chunks + // where the spacing is in the appended string rather set by appendSpace. + Paragraph P; + + P.appendText("after \n"); + EXPECT_EQ(P.asEscapedMarkdown(), "after"); + EXPECT_EQ(P.asMarkdown(), "after"); + EXPECT_EQ(P.asPlainText(), "after"); + + P.appendText(" foobar\n"); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar"); + + P.appendText("- bat\n"); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n\\- bat"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat"); + + P.appendText("- baz"); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n\\- bat\n\\- baz"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat\n- baz"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat\n- baz"); + + P.appendText(" faz "); + EXPECT_EQ(P.asEscapedMarkdown(), "after \n foobar\n\\- bat\n\\- baz faz"); + EXPECT_EQ(P.asMarkdown(), "after \n foobar\n- bat\n- baz faz"); + EXPECT_EQ(P.asPlainText(), "after\nfoobar\n- bat\n- baz faz"); } TEST(Paragraph, ExtraSpaces) { - // Make sure spaces inside chunks are dropped. + // Make sure spaces inside chunks are preserved for markdown + // and dropped for plain text. Paragraph P; P.appendText("foo\n \t baz"); P.appendCode(" bar\n"); - EXPECT_EQ(P.asMarkdown(), "foo baz`bar`"); + EXPECT_EQ(P.asEscapedMarkdown(), "foo\n \t baz`bar`"); + EXPECT_EQ(P.asMarkdown(), "foo\n \t baz`bar`"); EXPECT_EQ(P.asPlainText(), "foo bazbar"); } @@ -197,7 +401,8 @@ TEST(Paragraph, SpacesCollapsed) { Paragraph P; P.appendText(" foo bar "); P.appendText(" baz "); - EXPECT_EQ(P.asMarkdown(), "foo bar baz"); + EXPECT_EQ(P.asEscapedMarkdown(), "foo bar baz"); + EXPECT_EQ(P.asMarkdown(), "foo bar baz"); EXPECT_EQ(P.asPlainText(), "foo bar baz"); } @@ -206,21 +411,60 @@ TEST(Paragraph, NewLines) { Paragraph P; P.appendText(" \n foo\nbar\n "); P.appendCode(" \n foo\nbar \n "); - EXPECT_EQ(P.asMarkdown(), "foo bar `foo bar`"); + EXPECT_EQ(P.asEscapedMarkdown(), "foo\nbar\n `foo bar`"); + EXPECT_EQ(P.asMarkdown(), "foo\nbar\n `foo bar`"); EXPECT_EQ(P.asPlainText(), "foo bar foo bar"); } +TEST(Paragraph, BoldText) { + Paragraph P; + P.appendBoldText(""); + EXPECT_EQ(P.asEscapedMarkdown(), ""); + EXPECT_EQ(P.asMarkdown(), ""); + EXPECT_EQ(P.asPlainText(), ""); + + P.appendBoldText(" \n foo\nbar\n "); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*\\*foo bar\\*\\*"); + EXPECT_EQ(P.asMarkdown(), "**foo bar**"); + EXPECT_EQ(P.asPlainText(), "**foo bar**"); + + P.appendSpace().appendBoldText("foobar"); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*\\*foo bar\\*\\* \\*\\*foobar\\*\\*"); + EXPECT_EQ(P.asMarkdown(), "**foo bar** **foobar**"); + EXPECT_EQ(P.asPlainText(), "**foo bar** **foobar**"); +} + +TEST(Paragraph, EmphasizedText) { + Paragraph P; + P.appendEmphasizedText(""); + EXPECT_EQ(P.asEscapedMarkdown(), ""); + EXPECT_EQ(P.asMarkdown(), ""); + EXPECT_EQ(P.asPlainText(), ""); + + P.appendEmphasizedText(" \n foo\nbar\n "); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*foo bar\\*"); + EXPECT_EQ(P.asMarkdown(), "*foo bar*"); + EXPECT_EQ(P.asPlainText(), "*foo bar*"); + + P.appendSpace().appendEmphasizedText("foobar"); + EXPECT_EQ(P.asEscapedMarkdown(), "\\*foo bar\\* \\*foobar\\*"); + EXPECT_EQ(P.asMarkdown(), "*foo bar* *foobar*"); + EXPECT_EQ(P.asPlainText(), "*foo bar* *foobar*"); +} + TEST(Document, Separators) { Document D; D.addParagraph().appendText("foo"); D.addCodeBlock("test"); D.addParagraph().appendText("bar"); - const char ExpectedMarkdown[] = R"md(foo + const char ExpectedMarkdown[] = R"md(foo + ```cpp test ``` bar)md"; + EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); const char ExpectedText[] = R"pt(foo @@ -238,7 +482,8 @@ TEST(Document, Ruler) { // Ruler followed by paragraph. D.addParagraph().appendText("bar"); - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\nbar"); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\nbar"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nbar"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar"); D = Document(); @@ -246,7 +491,8 @@ TEST(Document, Ruler) { D.addRuler(); D.addCodeBlock("bar"); // Ruler followed by a codeblock. - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\n```cpp\nbar\n```"); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\n```cpp\nbar\n```"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\n```cpp\nbar\n```"); EXPECT_EQ(D.asPlainText(), "foo\n\nbar"); // Ruler followed by another ruler @@ -254,13 +500,15 @@ TEST(Document, Ruler) { D.addParagraph().appendText("foo"); D.addRuler(); D.addRuler(); + EXPECT_EQ(D.asEscapedMarkdown(), "foo"); EXPECT_EQ(D.asMarkdown(), "foo"); EXPECT_EQ(D.asPlainText(), "foo"); // Multiple rulers between blocks D.addRuler(); D.addParagraph().appendText("foo"); - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\nfoo"); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\nfoo"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nfoo"); EXPECT_EQ(D.asPlainText(), "foo\n\nfoo"); } @@ -272,7 +520,8 @@ TEST(Document, Append) { E.addRuler(); E.addParagraph().appendText("bar"); D.append(std::move(E)); - EXPECT_EQ(D.asMarkdown(), "foo \n\n---\nbar"); + EXPECT_EQ(D.asEscapedMarkdown(), "foo\n\n---\nbar"); + EXPECT_EQ(D.asMarkdown(), "foo\n\n---\nbar"); } TEST(Document, Heading) { @@ -280,8 +529,9 @@ TEST(Document, Heading) { D.addHeading(1).appendText("foo"); D.addHeading(2).appendText("bar"); D.addParagraph().appendText("baz"); - EXPECT_EQ(D.asMarkdown(), "# foo \n## bar \nbaz"); - EXPECT_EQ(D.asPlainText(), "foo\nbar\nbaz"); + EXPECT_EQ(D.asEscapedMarkdown(), "# foo\n\n## bar\n\nbaz"); + EXPECT_EQ(D.asMarkdown(), "# foo\n\n## bar\n\nbaz"); + EXPECT_EQ(D.asPlainText(), "foo\n\nbar\n\nbaz"); } TEST(CodeBlock, Render) { @@ -299,6 +549,7 @@ foo R"pt(foo bar baz)pt"; + EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asPlainText(), ExpectedPlainText); D.addCodeBlock("foo"); @@ -311,6 +562,7 @@ foo ```cpp foo ```)md"; + EXPECT_EQ(D.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(D.asMarkdown(), ExpectedMarkdown); ExpectedPlainText = R"pt(foo @@ -325,18 +577,20 @@ TEST(BulletList, Render) { BulletList L; // Flat list L.addItem().addParagraph().appendText("foo"); + EXPECT_EQ(L.asEscapedMarkdown(), "- foo"); EXPECT_EQ(L.asMarkdown(), "- foo"); EXPECT_EQ(L.asPlainText(), "- foo"); L.addItem().addParagraph().appendText("bar"); llvm::StringRef Expected = R"md(- foo - bar)md"; + EXPECT_EQ(L.asEscapedMarkdown(), Expected); EXPECT_EQ(L.asMarkdown(), Expected); EXPECT_EQ(L.asPlainText(), Expected); // Nested list, with a single item. Document &D = L.addItem(); - // First item with foo\nbaz + // First item with 2 paragraphs - foo\n\n baz D.addParagraph().appendText("foo"); D.addParagraph().appendText("baz"); @@ -352,18 +606,27 @@ TEST(BulletList, Render) { DeepDoc.addParagraph().appendText("baz"); StringRef ExpectedMarkdown = R"md(- foo - bar -- foo - baz - - foo - - baz +- foo + + baz + + - foo + + - baz + baz)md"; + EXPECT_EQ(L.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(L.asMarkdown(), ExpectedMarkdown); StringRef ExpectedPlainText = R"pt(- foo - bar - foo + baz + - foo + - baz + baz)pt"; EXPECT_EQ(L.asPlainText(), ExpectedPlainText); @@ -371,21 +634,31 @@ TEST(BulletList, Render) { Inner.addParagraph().appendText("after"); ExpectedMarkdown = R"md(- foo - bar -- foo - baz - - foo - - baz +- foo + + baz + + - foo + + - baz + baz - + after)md"; + EXPECT_EQ(L.asEscapedMarkdown(), ExpectedMarkdown); EXPECT_EQ(L.asMarkdown(), ExpectedMarkdown); ExpectedPlainText = R"pt(- foo - bar - foo + baz + - foo + - baz + baz + after)pt"; EXPECT_EQ(L.asPlainText(), ExpectedPlainText); } diff --git a/clang-tools-extra/docs/ReleaseNotes.rst b/clang-tools-extra/docs/ReleaseNotes.rst index cf97ab7082472..61debd89becaa 100644 --- a/clang-tools-extra/docs/ReleaseNotes.rst +++ b/clang-tools-extra/docs/ReleaseNotes.rst @@ -58,9 +58,6 @@ Semantic Highlighting Compile flags ^^^^^^^^^^^^^ -- Added `BuiltinHeaders` config key which controls whether clangd's built-in - headers are used or ones extracted from the driver. - Hover ^^^^^ @@ -88,93 +85,26 @@ Improvements to clang-doc Improvements to clang-query --------------------------- -Improvements to include-cleaner -------------------------------- -- Deprecated the ``-insert`` and ``-remove`` command line options, and added - the ``-disable-remove`` and ``-disable-insert`` command line options as - replacements. The previous command line options were confusing because they - did not imply the default state of the option (which is inserts and removes - being enabled). The new options are easier to understand the semantics of. +- Matcher queries interpreted by clang-query are now support trailing comma (,) + in matcher arguments. Note that C++ still doesn't allow this in function + arguments. So when porting a query to C++, remove all instances of trailing + comma (otherwise C++ compiler will just complain about "expected expression"). Improvements to clang-tidy -------------------------- -- Changed the :program:`check_clang_tidy.py` tool to use FileCheck's - ``--match-full-lines`` instead of ``strict-whitespace`` for ``CHECK-FIXES`` - clauses. Added a ``--match-partial-fixes`` option to keep previous behavior on - specific tests. This may break tests for users with custom out-of-tree checks - who use :program:`check_clang_tidy.py` as-is. - -- Improved :program:`clang-tidy-diff.py` script. Add the `-warnings-as-errors` - argument to treat warnings as errors. - -- Improved :program:`clang-tidy` to show `CheckOptions` only for checks enabled - in `Checks` when running ``--dump-config``. - -- Fixed bug in :program:`clang-tidy` by which `HeaderFilterRegex` did not take - effect when passed via the `.clang-tidy` file. - -- Fixed bug in :program:`run_clang_tidy.py` where the program would not - correctly display the checks enabled by the top-level `.clang-tidy` file. +- The :program:`run-clang-tidy.py` and :program:`clang-tidy-diff.py` scripts + now run checks in parallel by default using all available hardware threads. + Both scripts display the number of threads being used in their output. New checks ^^^^^^^^^^ -- New :doc:`bugprone-capturing-this-in-member-variable - ` check. - - Finds lambda captures and ``bind`` function calls that capture the ``this`` - pointer and store it as class members without handle the copy and move - constructors and the assignments. - -- New :doc:`bugprone-misleading-setter-of-reference - ` check. - - Finds setter-like member functions that take a pointer parameter and set a - reference member of the same class with the pointed value. - -- New :doc:`bugprone-unintended-char-ostream-output - ` check. - - Finds unintended character output from ``unsigned char`` and ``signed char`` - to an ``ostream``. - -- New :doc:`cppcoreguidelines-use-enum-class - ` check. - - Finds unscoped (non-class) ``enum`` declarations and suggests using - ``enum class`` instead. +- New :doc:`llvm-mlir-op-builder + ` check. -- New :doc:`llvm-prefer-static-over-anonymous-namespace - ` check. - - Finds function and variable declarations inside anonymous namespace and - suggests replacing them with ``static`` declarations. - -- New :doc:`modernize-use-scoped-lock - ` check. - - Finds uses of ``std::lock_guard`` and suggests replacing them with C++17's - alternative ``std::scoped_lock``. - -- New :doc:`portability-avoid-pragma-once - ` check. - - Finds uses of ``#pragma once`` and suggests replacing them with standard - include guards (``#ifndef``/``#define``/``#endif``) for improved portability. - -- New :doc:`readability-ambiguous-smartptr-reset-call - ` check. - - Finds potentially erroneous calls to ``reset`` method on smart pointers when - the pointee type also has a ``reset`` method. - -- New :doc:`readability-use-concise-preprocessor-directives - ` check. - - Finds uses of ``#if`` that can be simplified to ``#ifdef`` or ``#ifndef`` and, - since C23 and C++23, uses of ``#elif`` that can be simplified to ``#elifdef`` - or ``#elifndef``. + Checks for uses of MLIR's old/to be deprecated ``OpBuilder::create`` form + and suggests using ``T::create`` instead. New check aliases ^^^^^^^^^^^^^^^^^ @@ -182,206 +112,39 @@ New check aliases Changes in existing checks ^^^^^^^^^^^^^^^^^^^^^^^^^^ -- Improved :doc:`bugprone-crtp-constructor-accessibility - ` check by fixing - false positives on deleted constructors that cannot be used to construct - objects, even if they have public or protected access. - -- Improved :doc:`bugprone-exception-escape - ` check to print stack trace - of a potentially escaped exception. - -- Added an option to :doc:`bugprone-multi-level-implicit-pointer-conversion - ` to - choose whether to enable the check in C code or not. - -- Improved :doc:`bugprone-optional-value-conversion - ` check to detect - conversion in argument of ``std::make_optional``. - -- Improved :doc:`bugprone-sizeof-expression - ` check by adding - `WarnOnSizeOfInLoopTermination` option to detect misuses of ``sizeof`` - expression in loop conditions. - -- Improved :doc:`bugprone-string-constructor - ` check to find suspicious - calls of ``std::string`` constructor with char pointer, start position and - length parameters. - -- Improved :doc:`bugprone-unchecked-optional-access - ` fixing false - positives from smart pointer accessors repeated in checking ``has_value`` - and accessing ``value``. The option `IgnoreSmartPointerDereference` should - no longer be needed and will be removed. Also fixing false positive from - const reference accessors to objects containing optional member. - -- Improved :doc:`bugprone-unsafe-functions - ` check to allow specifying - additional C++ member functions to match. - -- Improved :doc:`cert-err33-c - ` check by fixing false positives when - a function name is just prefixed with a targeted function name. - -- Improved :doc:`concurrency-mt-unsafe - ` check by fixing a false positive - where ``strerror`` was flagged as MT-unsafe. - -- Improved :doc:`cppcoreguidelines-avoid-goto - ` check by adding the option - `IgnoreMacros` to ignore ``goto`` labels defined in macros. - -- Improved :doc:`cppcoreguidelines-interfaces-global-init - ` check by - fixing false positives on uses of ``constinit`` variables. - -- Improved :doc:`cppcoreguidelines-missing-std-forward - ` check by adding a - flag to specify the function used for forwarding instead of ``std::forward``. - -- Improved :doc:`cppcoreguidelines-pro-bounds-pointer-arithmetic - ` check by - fixing false positives when calling indexing operators that do not perform - pointer arithmetic in template, for example ``std::map::operator[]`` and - when pointer arithmetic was used through type aliases. - -- Improved :doc:`cppcoreguidelines-rvalue-reference-param-not-moved - ` check - by adding a flag to specify the function used for moving instead of - ``std::move``. - -- Improved :doc:`cppcoreguidelines-special-member-functions - ` check by - adding the option `IgnoreMacros` to ignore classes defined in macros. - -- Improved :doc:`google-readability-namespace-comments - ` check by adding - the option `AllowOmittingNamespaceComments` to accept if a namespace comment - is omitted entirely. - -- Improved :doc:`hicpp-avoid-goto - ` check by adding the option - `IgnoreMacros` to ignore ``goto`` labels defined in macros. - -- Improved :doc:`hicpp-special-member-functions - ` check by adding the - option `IgnoreMacros` to ignore classes defined in macros. - -- Improved :doc:`llvm-namespace-comment - ` check by adding the option - `AllowOmittingNamespaceComments` to accept if a namespace comment is omitted - entirely. - -- Improved :doc:`misc-const-correctness - ` check by adding the option - `AllowedTypes`, that excludes specified types from const-correctness - checking and fixing false positives when modifying variant by ``operator[]`` - with template in parameters and supporting to check pointee mutation by - `AnalyzePointers` option and fixing false positives when using const array - type. - -- Improved :doc:`misc-include-cleaner - ` check by adding the options - `UnusedIncludes` and `MissingIncludes`, which specify whether the check should - report unused or missing includes respectively. - -- Improved :doc:`misc-redundant-expression - ` check by providing additional - examples and fixing some macro related false positives. - -- Improved :doc:`misc-unconventional-assign-operator - ` check by fixing - false positives when copy assignment operator function in a template class - returns the result of another assignment to ``*this`` (``return *this=...``). - -- Improved :doc:`misc-unused-using-decls - ` check by fixing false positives - on ``operator""`` with template parameters. - -- Improved :doc:`misc-use-internal-linkage - ` check by fix false positives - for function or variable in header file which contains macro expansion and - excluding variables with ``thread_local`` storage class specifier from being - matched. - -- Improved :doc:`modernize-pass-by-value - ` check by fixing false positives - when class passed by const-reference had a private move constructor. - -- Improved :doc:`modernize-type-traits - ` check by detecting more type traits. - -- Improved :doc:`modernize-use-default-member-init - ` check by matching - arithmetic operations, ``constexpr`` and ``static`` values, and detecting - explicit casting of built-in types within member list initialization. +- Improved :doc:`bugprone-infinite-loop + ` check by adding detection for + variables introduced by structured bindings. + +- Improved :doc:`bugprone-signed-char-misuse + ` check by fixing + false positives on C23 enums with the fixed underlying type of signed char. + +- Improved :doc:`bugprone-unhandled-self-assignment + ` check by adding + an additional matcher that generalizes the copy-and-swap idiom pattern + detection. + +- Improved :doc:`misc-header-include-cycle + ` check performance. - Improved :doc:`modernize-use-designated-initializers - ` check by avoiding - diagnosing designated initializers for ``std::array`` initializations. - -- Improved :doc:`modernize-use-ranges - ` check by updating suppress - warnings logic for ``nullptr`` in ``std::find``. - -- Improved :doc:`modernize-use-starts-ends-with - ` check by adding more - matched scenarios of ``find`` and ``rfind`` methods and fixing false - positives when those methods were called with 3 arguments. - -- Improved :doc:`modernize-use-std-numbers - ` check to support math - functions of different precisions. - -- Improved :doc:`modernize-use-trailing-return-type - ` check by adding - support to modernize lambda signatures to use trailing return type and adding - two new options: `TransformFunctions` and `TransformLambdas` to control - whether function declarations and lambdas should be transformed by the check. - Fixed false positives when lambda was matched as a function in C++11 mode. - -- Improved :doc:`performance-move-const-arg - ` check by fixing false - negatives on ternary operators calling ``std::move``. - -- Improved :doc:`performance-unnecessary-value-param - ` check performance by - tolerating fix-it breaking compilation when functions is used as pointers - to avoid matching usage of functions within the current compilation unit. - Added an option `IgnoreCoroutines` with the default value `true` to - suppress this check for coroutines where passing by reference may be unsafe. - -- Improved :doc:`readability-convert-member-functions-to-static - ` check by - fixing false positives on member functions with an explicit object parameter. - -- Improved :doc:`readability-function-size - ` check by adding new option - `CountMemberInitAsStmt` that allows counting class member initializers in - constructors as statements. - -- Improved :doc:`readability-math-missing-parentheses - ` check by fixing - false negatives where math expressions are the operand of assignment operators - or comparison operators. - -- Improved :doc:`readability-named-parameter - ` check by adding the option - `InsertPlainNamesInForwardDecls` to insert parameter names without comments - for forward declarations only. - -- Improved :doc:`readability-qualified-auto - ` check by adding the option - `AllowedTypes`, that excludes specified types from adding qualifiers. - -- Improved :doc:`readability-redundant-inline-specifier - ` check by fixing - false positives on out-of-line explicitly defaulted functions. - -- Improved :doc:`readability-redundant-smartptr-get - ` check by fixing - some false positives involving smart pointers to arrays. + ` check to + suggest using designated initializers for aliased aggregate types. + +- Improved :doc:`modernize-use-std-format + ` check to correctly match + when the format string is converted to a different type by an implicit + constructor call. + +- Improved :doc:`modernize-use-std-print + ` check to correctly match + when the format string is converted to a different type by an implicit + constructor call. + +- Improved :doc:`portability-template-virtual-member-function + ` check to + avoid false positives on pure virtual member functions. Removed checks ^^^^^^^^^^^^^^ diff --git a/clang-tools-extra/docs/clang-tidy/Contributing.rst b/clang-tools-extra/docs/clang-tidy/Contributing.rst index 9611c655886f2..ad12b2343d1e9 100644 --- a/clang-tools-extra/docs/clang-tidy/Contributing.rst +++ b/clang-tools-extra/docs/clang-tidy/Contributing.rst @@ -19,11 +19,11 @@ check, the rest of this document explains how to do this. There are a few tools particularly useful when developing clang-tidy checks: * ``add_new_check.py`` is a script to automate the process of adding a new - check, it will create the check, update the CMake file and create a test; + check; it will create the check, update the CMake file and create a test. * ``rename_check.py`` does what the script name suggests, renames an existing - check; + check. * :program:`pp-trace` logs method calls on `PPCallbacks` for a source file - and is invaluable in understanding the preprocessor mechanism; + and is invaluable in understanding the preprocessor mechanism. * :program:`clang-query` is invaluable for interactive prototyping of AST matchers and exploration of the Clang AST; * `clang-check`_ with the ``-ast-dump`` (and optionally ``-ast-dump-filter``) @@ -47,7 +47,7 @@ implemented as a: + *Clang diagnostic*: if the check is generic enough, targets code patterns that most probably are bugs (rather than style or readability issues), can be - implemented effectively and with extremely low false positive rate, it may + implemented effectively and with extremely low false-positive rate, it may make a good Clang diagnostic. + *Clang static analyzer check*: if the check requires some sort of control flow @@ -77,7 +77,7 @@ make sure that you enable the ``clang`` and ``clang-tools-extra`` projects to build :program:`clang-tidy`. Because your new check will have associated documentation, you will also want to install `Sphinx `_ and enable it in the CMake configuration. -To save build time of the core Clang libraries you may want to only enable the ``X86`` +To save build time of the core Clang libraries, you may want to only enable the ``X86`` target in the CMake configuration. @@ -130,7 +130,7 @@ So you have an idea of a useful check for :program:`clang-tidy`. First, if you're not familiar with LLVM development, read through the `Getting Started with the LLVM System`_ document for instructions on setting up your workflow and the `LLVM Coding Standards`_ document to familiarize yourself with the coding -style used in the project. For code reviews we currently use `LLVM Github`_, +style used in the project. For code reviews, we currently use `LLVM Github`_, though historically we used Phabricator. .. _Getting Started with the LLVM System: https://llvm.org/docs/GettingStarted.html @@ -141,7 +141,7 @@ Next, you need to decide which module the check belongs to. Modules are located in subdirectories of `clang-tidy/ `_ and contain checks targeting a certain aspect of code quality (performance, -readability, etc.), certain coding style or standard (Google, LLVM, CERT, etc.) +readability, etc.), a certain coding style or standard (Google, LLVM, CERT, etc.) or a widely used API (e.g. MPI). Their names are the same as the user-facing check group names described :ref:`above `. @@ -166,7 +166,7 @@ The ``add_new_check.py`` script will: * create a documentation file and include it into the ``docs/clang-tidy/checks/list.rst``. -Let's see in more detail at the check class definition: +Let's look at the check class definition in more detail: .. code-block:: c++ @@ -200,7 +200,7 @@ In our case the check needs to operate on the AST level and it overrides the preprocessor level, we'd need instead to override the ``registerPPCallbacks`` method. -In the ``registerMatchers`` method we create an AST Matcher (see `AST Matchers`_ +In the ``registerMatchers`` method, we create an AST Matcher (see `AST Matchers`_ for more information) that will find the pattern in the AST that we want to inspect. The results of the matching are passed to the ``check`` method, which can further inspect them and report diagnostics. @@ -320,7 +320,7 @@ the ``add_new_check.py`` script: Developing your check incrementally ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The best way to develop your check is to start with the simple test cases and increase +The best way to develop your check is to start with simple test cases and increase complexity incrementally. The test file created by the ``add_new_check.py`` script is a starting point for your test cases. A rough outline of the process looks like this: @@ -393,7 +393,7 @@ good way to catch things you forgot to account for in your matchers. However, t LLVM code base may be insufficient for testing purposes as it was developed against a particular set of coding styles and quality measures. The larger the corpus of code the check is tested against, the higher confidence the community will have in the -check's efficacy and false positive rate. +check's efficacy and false-positive rate. Some suggestions to ensure your check is robust: @@ -406,10 +406,10 @@ Some suggestions to ensure your check is robust: - Define template classes that contain code matched by your check. - Define template specializations that contain code matched by your check. - Test your check under both Windows and Linux environments. -- Watch out for high false positive rates. Ideally, a check would have no false +- Watch out for high false-positive rates. Ideally, a check would have no false positives, but given that matching against an AST is not control- or data flow- - sensitive, a number of false positives are expected. The higher the false - positive rate, the less likely the check will be adopted in practice. + sensitive, a number of false positives are expected. The higher the + false-positive rate, the less likely the check will be adopted in practice. Mechanisms should be put in place to help the user manage false positives. - There are two primary mechanisms for managing false positives: supporting a code pattern which allows the programmer to silence the diagnostic in an ad @@ -428,10 +428,10 @@ Documenting your check The ``add_new_check.py`` script creates entries in the `release notes `_, the list of checks and a new file for the check documentation itself. It is recommended that you -have a concise summation of what your check does in a single sentence that is repeated +have a concise summary of what your check does in a single sentence that is repeated in the release notes, as the first sentence in the doxygen comments in the header file for your check class and as the first sentence of the check documentation. Avoid the -phrase "this check" in your check summation and check documentation. +phrase "this check" in your check summary and check documentation. If your check relates to a published coding guideline (C++ Core Guidelines, MISRA, etc.) or style guide, provide links to the relevant guideline or style guide sections in your @@ -443,10 +443,10 @@ If there are exceptions or limitations to your check, document them thoroughly. will help users understand the scope of the diagnostics and fix-its provided by the check. Building the target ``docs-clang-tools-html`` will run the Sphinx documentation generator -and create documentation HTML files in the tools/clang/tools/extra/docs/html directory in +and create HTML documentation files in the tools/clang/tools/extra/docs/html directory in your build tree. Make sure that your check is correctly shown in the release notes and the list of checks. Make sure that the formatting and structure of your check's documentation -looks correct. +look correct. Registering your Check @@ -503,11 +503,11 @@ Configuring Checks If a check needs configuration options, it can access check-specific options using the ``Options.get("SomeOption", DefaultValue)`` call in the check -constructor. In this case the check should also override the +constructor. In this case, the check should also override the ``ClangTidyCheck::storeOptions`` method to make the options provided by the check discoverable. This method lets :program:`clang-tidy` know which options the check implements and what the current values are (e.g. for the -``-dump-config`` command line option). +``-dump-config`` command-line option). .. code-block:: c++ @@ -576,7 +576,7 @@ typically the basic ``CHECK`` forms (``CHECK-MESSAGES`` and ``CHECK-FIXES``) are sufficient for clang-tidy tests. Note that the `FileCheck`_ documentation mostly assumes the default prefix (``CHECK``), and hence describes the directive as ``CHECK:``, ``CHECK-SAME:``, ``CHECK-NOT:``, etc. -Replace ``CHECK`` by either ``CHECK-FIXES`` or ``CHECK-MESSAGES`` for +Replace ``CHECK`` with either ``CHECK-FIXES`` or ``CHECK-MESSAGES`` for clang-tidy tests. An additional check enabled by ``check_clang_tidy.py`` ensures that @@ -590,7 +590,7 @@ appropriate ``RUN`` line in the ``test/clang-tidy`` directory. Use diagnostic messages and fixed code. It's advised to make the checks as specific as possible to avoid checks matching -to incorrect parts of the input. Use ``[[@LINE+X]]``/``[[@LINE-X]]`` +incorrect parts of the input. Use ``[[@LINE+X]]``/``[[@LINE-X]]`` substitutions and distinct function and variable names in the test code. Here's an example of a test using the ``check_clang_tidy.py`` script (the full @@ -606,7 +606,7 @@ source code is at `test/clang-tidy/checkers/google/readability-casting.cpp`_): // CHECK-FIXES: int b = a; } -To check more than one scenario in the same test file use +To check more than one scenario in the same test file, use ``-check-suffix=SUFFIX-NAME`` on ``check_clang_tidy.py`` command line or ``-check-suffixes=SUFFIX-NAME-1,SUFFIX-NAME-2,...``. With ``-check-suffix[es]=SUFFIX-NAME`` you need to replace your ``CHECK-*`` @@ -631,15 +631,15 @@ There are many dark corners in the C++ language, and it may be difficult to make your check work perfectly in all cases, especially if it issues fix-it hints. The most frequent pitfalls are macros and templates: -1. code written in a macro body/template definition may have a different meaning - depending on the macro expansion/template instantiation; -2. multiple macro expansions/template instantiations may result in the same code +1. Code written in a macro body/template definition may have a different meaning + depending on the macro expansion/template instantiation. +2. Multiple macro expansions/template instantiations may result in the same code being inspected by the check multiple times (possibly, with different meanings, see 1), and the same warning (or a slightly different one) may be issued by the check multiple times; :program:`clang-tidy` will deduplicate _identical_ warnings, but if the warnings are slightly different, all of them - will be shown to the user (and used for applying fixes, if any); -3. making replacements to a macro body/template definition may be fine for some + will be shown to the user (and used for applying fixes, if any). +3. Making replacements to a macro body/template definition may be fine for some macro expansions/template instantiations, but easily break some other expansions/instantiations. @@ -657,6 +657,29 @@ directory. The path to this directory is available in a lit test with the varia .. _FileCheck: https://llvm.org/docs/CommandGuide/FileCheck.html .. _test/clang-tidy/checkers/google/readability-casting.cpp: https://github.com/llvm/llvm-project/blob/main/clang-tools-extra/test/clang-tidy/checkers/google/readability-casting.cpp + +Submitting a Pull Request +------------------------- + +Before submitting a pull request, contributors are encouraged to run +:program:`clang-tidy` and :program:`clang-format` on their changes to ensure +code quality and catch potential issues. While :program:`clang-tidy` is not +currently enforced in CI, following this practice helps maintain code +consistency and prevent common errors. + +Here's a useful command to check your staged changes: + +.. code-block:: console + + $ git diff --staged -U0 | ./clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py \ + -j $(nproc) -path build/ -p1 -only-check-in-db + $ git clang-format + +Note that some warnings may be false positives or require careful consideration +before fixing. Use your judgment and feel free to discuss in the pull request +if you're unsure about a particular warning. + + Out-of-tree check plugins ------------------------- @@ -675,7 +698,7 @@ names of the checks to enable. $ clang-tidy --checks=-*,my-explicit-constructor -list-checks -load myplugin.so -There is no expectations regarding ABI and API stability, so the plugin must be +There are no expectations regarding ABI and API stability, so the plugin must be compiled against the version of clang-tidy that will be loading the plugin. The plugins can use threads, TLS, or any other facilities available to in-tree @@ -697,10 +720,10 @@ and write a version of `check_clang_tidy.py`_ to suit your needs. Running clang-tidy on LLVM -------------------------- -To test a check it's best to try it out on a larger code base. LLVM and Clang +To test a check, it's best to try it out on a larger code base. LLVM and Clang are the natural targets as you already have the source code around. The most convenient way to run :program:`clang-tidy` is with a compile command database; -CMake can automatically generate one, for a description of how to enable it see +CMake can automatically generate one; for a description of how to enable it, see `How To Setup Clang Tooling For LLVM`_. Once ``compile_commands.json`` is in place and a working version of :program:`clang-tidy` is in ``PATH`` the entire code base can be analyzed with ``clang-tidy/tool/run-clang-tidy.py``. The script @@ -712,18 +735,18 @@ warnings and errors. The script provides multiple configuration flags. * The default set of checks can be overridden using the ``-checks`` argument, - taking the identical format as :program:`clang-tidy` does. For example + taking the identical format as :program:`clang-tidy` does. For example, ``-checks=-*,modernize-use-override`` will run the ``modernize-use-override`` check only. -* To restrict the files examined you can provide one or more regex arguments +* To restrict the files examined, you can provide one or more regex arguments that the file names are matched against. ``run-clang-tidy.py clang-tidy/.*Check\.cpp`` will only analyze `clang-tidy` checks. It may also be necessary to restrict the header files that warnings are displayed from by using the ``-header-filter`` and ``-exclude-header-filter`` flags. They have the same behavior as the corresponding :program:`clang-tidy` flags. -* To apply suggested fixes ``-fix`` can be passed as an argument. This gathers +* To apply suggested fixes, ``-fix`` can be passed as an argument. This gathers all changes in a temporary directory and applies them. Passing ``-format`` will run clang-format over changed lines. @@ -772,7 +795,7 @@ There is only one argument that controls profile storage: * ``-store-check-profile=`` - By default reports are printed in tabulated format to stderr. When this option + By default, reports are printed in tabulated format to stderr. When this option is passed, these per-TU profiles are instead stored as JSON. If the prefix is not an absolute path, it is considered to be relative to the directory from where you have run :program:`clang-tidy`. All ``.`` and ``..`` diff --git a/clang-tools-extra/docs/clang-tidy/checks/list.rst b/clang-tools-extra/docs/clang-tidy/checks/list.rst index 0cffbd323caa2..20a43274f9788 100644 --- a/clang-tools-extra/docs/clang-tidy/checks/list.rst +++ b/clang-tools-extra/docs/clang-tidy/checks/list.rst @@ -247,6 +247,7 @@ Clang-Tidy Checks :doc:`linuxkernel-must-check-errs `, :doc:`llvm-header-guard `, :doc:`llvm-include-order `, "Yes" + :doc:`llvm-use-new-mlir-op-builder `, "Yes" :doc:`llvm-namespace-comment `, :doc:`llvm-prefer-isa-or-dyn-cast-in-conditionals `, "Yes" :doc:`llvm-prefer-register-over-unsigned `, "Yes" diff --git a/clang-tools-extra/docs/clang-tidy/checks/llvm/use-new-mlir-op-builder.rst b/clang-tools-extra/docs/clang-tidy/checks/llvm/use-new-mlir-op-builder.rst new file mode 100644 index 0000000000000..dc1989dc61176 --- /dev/null +++ b/clang-tools-extra/docs/clang-tidy/checks/llvm/use-new-mlir-op-builder.rst @@ -0,0 +1,21 @@ +.. title:: clang-tidy - llvm-use-new-mlir-op-builder + +llvm-mlir-op-builder +==================== + +Checks for uses of MLIR's old/to be deprecated ``OpBuilder::create`` form +and suggests using ``T::create`` instead. + +Example +------- + +.. code-block:: c++ + + builder.create(builder.getUnknownLoc(), "baz"); + + +Transforms to: + +.. code-block:: c++ + + FooOp::create(builder, builder.getUnknownLoc(), "baz"); diff --git a/clang-tools-extra/include-cleaner/lib/WalkAST.cpp b/clang-tools-extra/include-cleaner/lib/WalkAST.cpp index baff90faa6eae..49cc13606f4c2 100644 --- a/clang-tools-extra/include-cleaner/lib/WalkAST.cpp +++ b/clang-tools-extra/include-cleaner/lib/WalkAST.cpp @@ -140,7 +140,6 @@ class ASTWalker : public RecursiveASTVisitor { return true; switch (Qual->getKind()) { case NestedNameSpecifier::Namespace: - case NestedNameSpecifier::NamespaceAlias: case NestedNameSpecifier::Global: return true; case NestedNameSpecifier::TypeSpec: diff --git a/clang-tools-extra/test/clang-doc/Inputs/basic-project/include/Circle.h b/clang-tools-extra/test/clang-doc/Inputs/basic-project/include/Circle.h index 7bee3ffa92539..74bffcdec993b 100644 --- a/clang-tools-extra/test/clang-doc/Inputs/basic-project/include/Circle.h +++ b/clang-tools-extra/test/clang-doc/Inputs/basic-project/include/Circle.h @@ -26,6 +26,10 @@ class Circle : public Shape { /** * @brief Calculates the perimeter of the circle. * + * @code + * Circle circle(5.0); + * double perimeter = circle.perimeter(); + * @endcode * @return double The perimeter of the circle. */ double perimeter() const override; diff --git a/clang-tools-extra/test/clang-doc/basic-project.mustache.test b/clang-tools-extra/test/clang-doc/basic-project.mustache.test index 7bfdd4bb1dd3f..e2d9da60183fa 100644 --- a/clang-tools-extra/test/clang-doc/basic-project.mustache.test +++ b/clang-tools-extra/test/clang-doc/basic-project.mustache.test @@ -2,17 +2,17 @@ // RUN: sed 's|$test_dir|%/S|g' %S/Inputs/basic-project/database_template.json > %t/build/compile_commands.json // RUN: clang-doc --format=mustache --output=%t/docs --executor=all-TUs %t/build/compile_commands.json -// RUN: FileCheck %s -input-file=%t/docs/GlobalNamespace/Shape.html -check-prefix=HTML-SHAPE -// RUN: FileCheck %s -input-file=%t/docs/GlobalNamespace/Calculator.html -check-prefix=HTML-CALC -// RUN: FileCheck %s -input-file=%t/docs/GlobalNamespace/Rectangle.html -check-prefix=HTML-RECTANGLE -// RUN: FileCheck %s -input-file=%t/docs/GlobalNamespace/Circle.html -check-prefix=HTML-CIRCLE +// RUN: FileCheck %s -input-file=%t/docs/_ZTV5Shape.html -check-prefix=HTML-SHAPE +// RUN: FileCheck %s -input-file=%t/docs/_ZTV10Calculator.html -check-prefix=HTML-CALC +// RUN: FileCheck %s -input-file=%t/docs/_ZTV9Rectangle.html -check-prefix=HTML-RECTANGLE +// RUN: FileCheck %s -input-file=%t/docs/_ZTV6Circle.html -check-prefix=HTML-CIRCLE HTML-SHAPE: HTML-SHAPE: HTML-SHAPE: HTML-SHAPE: Shape -HTML-SHAPE: -HTML-SHAPE: +HTML-SHAPE: +HTML-SHAPE: HTML-SHAPE: HTML-SHAPE: HTML-SHAPE: @@ -60,6 +60,17 @@ HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE:

class Shape

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

Abstract base class for shapes.

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

Provides a common interface for different types of shapes.

+HTML-SHAPE:
+HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE:
@@ -72,6 +83,19 @@ HTML-SHAPE: HTML-SHAPE: double area () HTML-SHAPE: HTML-SHAPE: +HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

Calculates the area of the shape.

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

+HTML-SHAPE:
+HTML-SHAPE:

Returns

+HTML-SHAPE:

double The area of the shape.

+HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE: HTML-SHAPE:
@@ -81,6 +105,19 @@ HTML-SHAPE: HTML-SHAPE: double perimeter () HTML-SHAPE: HTML-SHAPE: +HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

Calculates the perimeter of the shape.

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

+HTML-SHAPE:
+HTML-SHAPE:

Returns

+HTML-SHAPE:

double The perimeter of the shape.

+HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE: HTML-SHAPE:
@@ -90,6 +127,14 @@ HTML-SHAPE: HTML-SHAPE: void ~Shape () HTML-SHAPE: HTML-SHAPE: +HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

Virtual destructor.

+HTML-SHAPE:
+HTML-SHAPE:
+HTML-SHAPE:

+HTML-SHAPE:
+HTML-SHAPE:
HTML-SHAPE:
HTML-SHAPE: HTML-SHAPE: @@ -106,8 +151,8 @@ HTML-CALC: HTML-CALC: HTML-CALC: HTML-CALC: Calculator -HTML-CALC: -HTML-CALC: +HTML-CALC: +HTML-CALC: HTML-CALC: HTML-CALC: HTML-CALC: @@ -135,7 +180,7 @@ HTML-CALC: HTML-CALC: HTML-CALC:
@@ -208,6 +293,19 @@ HTML-CALC: HTML-CALC: int subtract (int a, int b) HTML-CALC: HTML-CALC: +HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Subtracts the second integer from the first.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:

Returns

+HTML-CALC:

int The result of a - b.

+HTML-CALC:
HTML-CALC:
HTML-CALC: HTML-CALC:
@@ -217,6 +315,36 @@ HTML-CALC: HTML-CALC: int multiply (int a, int b) HTML-CALC: HTML-CALC: +HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Multiplies two integers.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:

Parameters

+HTML-CALC:
+HTML-CALC: a
+HTML-CALC:

First integer.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:
+HTML-CALC: b
+HTML-CALC:

Second integer.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Returns

+HTML-CALC:

int The product of a and b.

+HTML-CALC:
HTML-CALC:
HTML-CALC: HTML-CALC:
@@ -226,6 +354,37 @@ HTML-CALC: HTML-CALC: double divide (int a, int b) HTML-CALC: HTML-CALC: +HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Divides the first integer by the second.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:

Parameters

+HTML-CALC:
+HTML-CALC: a
+HTML-CALC:

First integer.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:
+HTML-CALC: b
+HTML-CALC:

Second integer.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Returns

+HTML-CALC:

double The result of a / b.

+HTML-CALC:

+HTML-CALC:
HTML-CALC:
HTML-CALC: HTML-CALC:
@@ -235,6 +394,36 @@ HTML-CALC: HTML-CALC: int mod (int a, int b) HTML-CALC: HTML-CALC: +HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Performs the mod operation on integers.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:

Parameters

+HTML-CALC:
+HTML-CALC: a
+HTML-CALC:

First integer.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:
+HTML-CALC: b
+HTML-CALC:

Second integer.

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

+HTML-CALC:
+HTML-CALC:
+HTML-CALC:

Returns

+HTML-CALC:

The result of a % b.

+HTML-CALC:
HTML-CALC:
HTML-CALC: HTML-CALC: @@ -251,8 +440,8 @@ HTML-RECTANGLE: HTML-RECTANGLE: HTML-RECTANGLE: HTML-RECTANGLE: Rectangle -HTML-RECTANGLE: -HTML-RECTANGLE: +HTML-RECTANGLE: +HTML-RECTANGLE: HTML-RECTANGLE: HTML-RECTANGLE: HTML-RECTANGLE: @@ -279,17 +468,6 @@ HTML-RECTANGLE:
HTML-RECTANGLE: HTML-RECTANGLE:
@@ -347,6 +546,19 @@ HTML-RECTANGLE: HTML-RECTANGLE: double area () HTML-RECTANGLE: HTML-RECTANGLE: +HTML-RECTANGLE:
+HTML-RECTANGLE:
+HTML-RECTANGLE:

Calculates the area of the rectangle.

+HTML-RECTANGLE:
+HTML-RECTANGLE:
+HTML-RECTANGLE:

+HTML-RECTANGLE:
+HTML-RECTANGLE:
+HTML-RECTANGLE:

+HTML-RECTANGLE:
+HTML-RECTANGLE:

Returns

+HTML-RECTANGLE:

double The area of the rectangle.

+HTML-RECTANGLE:
HTML-RECTANGLE:
HTML-RECTANGLE:
HTML-RECTANGLE:
@@ -356,6 +568,19 @@ HTML-RECTANGLE: HTML-RECTANGLE: double perimeter () HTML-RECTANGLE: HTML-RECTANGLE: +HTML-RECTANGLE:
+HTML-RECTANGLE:
+HTML-RECTANGLE:

Calculates the perimeter of the rectangle.

+HTML-RECTANGLE:
+HTML-RECTANGLE:
+HTML-RECTANGLE:

+HTML-RECTANGLE:
+HTML-RECTANGLE:
+HTML-RECTANGLE:

+HTML-RECTANGLE:
+HTML-RECTANGLE:

Returns

+HTML-RECTANGLE:

double The perimeter of the rectangle.

+HTML-RECTANGLE:
HTML-RECTANGLE:
HTML-RECTANGLE: HTML-RECTANGLE: @@ -372,8 +597,8 @@ HTML-CIRCLE: HTML-CIRCLE: HTML-CIRCLE: HTML-CIRCLE: Circle -HTML-CIRCLE: -HTML-CIRCLE: +HTML-CIRCLE: +HTML-CIRCLE: HTML-CIRCLE: HTML-CIRCLE: HTML-CIRCLE: @@ -400,14 +625,6 @@ HTML-CIRCLE:
HTML-CIRCLE: HTML-CIRCLE:
@@ -460,6 +695,19 @@ HTML-CIRCLE: HTML-CIRCLE: double area () HTML-CIRCLE: HTML-CIRCLE: +HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:

Calculates the area of the circle.

+HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:

+HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:

+HTML-CIRCLE:
+HTML-CIRCLE:

Returns

+HTML-CIRCLE:

double The area of the circle.

+HTML-CIRCLE:
HTML-CIRCLE:
HTML-CIRCLE:
HTML-CIRCLE:
@@ -469,6 +717,28 @@ HTML-CIRCLE: HTML-CIRCLE: double perimeter () HTML-CIRCLE: HTML-CIRCLE: +HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:

Calculates the perimeter of the circle.

+HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:

+HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:

+HTML-CIRCLE:
+HTML-CIRCLE:

Returns

+HTML-CIRCLE:

double The perimeter of the circle.

+HTML-CIRCLE:

Code

+HTML-CIRCLE:
+HTML-CIRCLE:
+HTML-CIRCLE:                            
+HTML-CIRCLE:                            Circle circle(5.0);
+HTML-CIRCLE:                            double perimeter = circle.perimeter();
+HTML-CIRCLE:                            
+HTML-CIRCLE:                        
+HTML-CIRCLE:
+HTML-CIRCLE:
HTML-CIRCLE:
HTML-CIRCLE: HTML-CIRCLE: diff --git a/clang-tools-extra/test/clang-doc/json/class-requires.cpp b/clang-tools-extra/test/clang-doc/json/class-requires.cpp index 213da93a1adfa..bf6c889849a70 100644 --- a/clang-tools-extra/test/clang-doc/json/class-requires.cpp +++ b/clang-tools-extra/test/clang-doc/json/class-requires.cpp @@ -20,6 +20,7 @@ struct MyClass; // CHECK-NEXT: "Template": { // CHECK-NEXT: "Constraints": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "Addable", // CHECK-NEXT: "Name": "Addable", // CHECK-NEXT: "Path": "", diff --git a/clang-tools-extra/test/clang-doc/json/class-template.cpp b/clang-tools-extra/test/clang-doc/json/class-template.cpp index 6cdc3e9175278..149248c772055 100644 --- a/clang-tools-extra/test/clang-doc/json/class-template.cpp +++ b/clang-tools-extra/test/clang-doc/json/class-template.cpp @@ -11,6 +11,7 @@ template struct MyClass { // CHECK: "Name": "method", // CHECK: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "Param", // CHECK-NEXT: "Type": "T" // CHECK-NEXT: } diff --git a/clang-tools-extra/test/clang-doc/json/class.cpp b/clang-tools-extra/test/clang-doc/json/class.cpp index d8317eafea91a..79b8fed0a0188 100644 --- a/clang-tools-extra/test/clang-doc/json/class.cpp +++ b/clang-tools-extra/test/clang-doc/json/class.cpp @@ -33,35 +33,29 @@ struct MyClass { }; // CHECK: { -// CHECK-NEXT: "Description": [ -// CHECK-NEXT: { -// CHECK-NEXT: "FullComment": { -// CHECK-NEXT: "Children": [ -// CHECK-NEXT: { -// CHECK-NEXT: "ParagraphComment": { -// CHECK-NEXT: "Children": [ -// CHECK-NEXT: { -// CHECK-NEXT: "TextComment": " This is a nice class." -// CHECK-NEXT: }, -// CHECK-NEXT: { -// CHECK-NEXT: "TextComment": " It has some nice methods and fields." -// CHECK-NEXT: }, -// CHECK-NEXT: { -// CHECK-NEXT: "TextComment": "" -// CHECK-NEXT: } -// CHECK-NEXT: ] -// CHECK: { -// CHECK-NEXT: "BlockCommandComment": { -// CHECK-NEXT: "Children": [ -// CHECK-NEXT: { -// CHECK-NEXT: "ParagraphComment": { -// CHECK-NEXT: "Children": [ -// CHECK-NEXT: { -// CHECK-NEXT: "TextComment": " This is a brief description." -// CHECK-NEXT: } -// CHECK: "Command": "brief" +// CHECK-NEXT: "Description": { +// CHECK-NEXT: "BriefComments": [ +// CHECK-NEXT: [ +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " This is a brief description." +// CHECK: "HasBriefComments": true, +// CHECK-NEXT: "HasParagraphComments": true, +// CHECK-NEXT: "ParagraphComments": [ +// CHECK-NEXT: [ +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " This is a nice class." +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " It has some nice methods and fields." +// CHECK-NEXT: }, +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": "" +// CHECK-NEXT: } +// CHECK: "DocumentationFileName": "_ZTV7MyClass", // CHECK: "Enums": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "enum", // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}class.cpp", // CHECK-NEXT: "LineNumber": 17 @@ -76,6 +70,7 @@ struct MyClass { // CHECK-NEXT: "Value": "1" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "BLUE", // CHECK-NEXT: "ValueExpr": "5" // CHECK-NEXT: } @@ -94,6 +89,7 @@ struct MyClass { // CHECK-NEXT: "IsClass": false, // CHECK-NEXT: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "", // CHECK-NEXT: "Type": "int" // CHECK-NEXT: } @@ -118,6 +114,7 @@ struct MyClass { // CHECK-NEXT: } // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "IsClass": true, // CHECK-NEXT: "Reference": { // CHECK-NEXT: "Name": "Foo", @@ -129,6 +126,11 @@ struct MyClass { // CHECK-NEXT: ], // COM: FIXME: FullName is not emitted correctly. // CHECK-NEXT: "FullName": "", +// CHECK-NEXT: "HasEnums": true, +// CHECK-NEXT: "HasPublicFunctions": true, +// CHECK-NEXT: "HasPublicMembers": true, +// CHECK-NEXT: "HasRecords": true, +// CHECK-NEXT: "InfoType": "record", // CHECK-NEXT: "IsTypedef": false, // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}class.cpp", @@ -142,6 +144,7 @@ struct MyClass { // CHECK-NEXT: "Path": "GlobalNamespace", // CHECK-NEXT: "ProtectedFunctions": [ // CHECK-NEXT: { +// CHECK-NEXT: "InfoType": "function", // CHECK-NEXT: "IsStatic": false, // CHECK-NEXT: "Name": "protectedMethod", // CHECK-NEXT: "Namespace": [ @@ -166,6 +169,7 @@ struct MyClass { // CHECK-NEXT: ], // CHECK-NEXT: "PublicFunctions": [ // CHECK-NEXT: { +// CHECK-NEXT: "InfoType": "function", // CHECK-NEXT: "IsStatic": false, // CHECK-NEXT: "Name": "myMethod", // CHECK-NEXT: "Namespace": [ @@ -174,6 +178,7 @@ struct MyClass { // CHECK-NEXT: ], // CHECK-NEXT: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "MyParam", // CHECK-NEXT: "Type": "int" // CHECK-NEXT: } @@ -204,6 +209,8 @@ struct MyClass { // CHECK-NEXT: ], // CHECK-NEXT: "Records": [ // CHECK-NEXT: { +// CHECK-NEXT: "DocumentationFileName": "_ZTVN7MyClass11NestedClassE", +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "NestedClass", // CHECK-NEXT: "Path": "GlobalNamespace{{[\/]+}}MyClass", // CHECK-NEXT: "QualName": "NestedClass", @@ -213,6 +220,8 @@ struct MyClass { // CHECK-NEXT: "TagType": "struct", // CHECK-NEXT: "Typedefs": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "typedef", // CHECK-NEXT: "IsUsing": false, // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}class.cpp", diff --git a/clang-tools-extra/test/clang-doc/json/compound-constraints.cpp b/clang-tools-extra/test/clang-doc/json/compound-constraints.cpp index 34acb6808409d..bb2b4ca770fc0 100644 --- a/clang-tools-extra/test/clang-doc/json/compound-constraints.cpp +++ b/clang-tools-extra/test/clang-doc/json/compound-constraints.cpp @@ -37,6 +37,7 @@ template requires (Incrementable && Decrementable) || PreIncre // CHECK-NEXT: "USR": "{{[0-9A-F]*}}" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "Decrementable", // CHECK-NEXT: "Name": "Decrementable", // CHECK-NEXT: "Path": "", @@ -55,6 +56,7 @@ template requires (Incrementable && Decrementable) || PreIncre // CHECK-NEXT: "USR": "{{[0-9A-F]*}}" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "Decrementable", // CHECK-NEXT: "Name": "Decrementable", // CHECK-NEXT: "Path": "", @@ -87,6 +89,7 @@ template requires (Incrementable && Decrementable) || PreIncre // CHECK-NEXT: "USR": "{{[0-9A-F]*}}" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "PreDecrementable", // CHECK-NEXT: "Name": "PreDecrementable", // CHECK-NEXT: "Path": "", @@ -112,6 +115,7 @@ template requires (Incrementable && Decrementable) || PreIncre // CHECK-NEXT: "USR": "{{[0-9A-F]*}}" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "PreIncrementable", // CHECK-NEXT: "Name": "PreIncrementable", // CHECK-NEXT: "Path": "", diff --git a/clang-tools-extra/test/clang-doc/json/concept.cpp b/clang-tools-extra/test/clang-doc/json/concept.cpp index b946393274c85..4c810244ca41b 100644 --- a/clang-tools-extra/test/clang-doc/json/concept.cpp +++ b/clang-tools-extra/test/clang-doc/json/concept.cpp @@ -13,16 +13,14 @@ concept Incrementable = requires(T x) { // CHECK-NEXT: "Concepts": [ // CHECK-NEXT: { // CHECK-NEXT: "ConstraintExpression": "requires (T x) { ++x; x++; }", -// CHECK-NEXT: "Description": [ -// CHECK-NEXT: { -// CHECK-NEXT: "FullComment": { -// CHECK-NEXT: "Children": [ -// CHECK-NEXT: { -// CHECK-NEXT: "ParagraphComment": { -// CHECK-NEXT: "Children": [ -// CHECK-NEXT: { -// CHECK-NEXT: "TextComment": " Requires that T suports post and pre-incrementing." -// CHECK: ], +// CHECK-NEXT: "Description": { +// CHECK-NEXT: "HasParagraphComments": true, +// CHECK-NEXT: "ParagraphComments": [ +// CHECK-NEXT: [ +// CHECK-NEXT: { +// CHECK-NEXT: "TextComment": " Requires that T suports post and pre-incrementing." +// CHECK: "End": true, +// CHECK-NEXT: "InfoType": "concept", // CHECK-NEXT: "IsType": true, // CHECK-NEXT: "Name": "Incrementable", // CHECK-NEXT: "Template": { diff --git a/clang-tools-extra/test/clang-doc/json/function-requires.cpp b/clang-tools-extra/test/clang-doc/json/function-requires.cpp index 08ac4c7ed2ca3..59ed39ee61fda 100644 --- a/clang-tools-extra/test/clang-doc/json/function-requires.cpp +++ b/clang-tools-extra/test/clang-doc/json/function-requires.cpp @@ -14,10 +14,12 @@ template Incrementable auto incrementTwo(T t); // CHECK: "Functions": [ // CHECK-NEXT: { +// CHECK-NEXT: "InfoType": "function", // CHECK-NEXT: "IsStatic": false, // CHECK-NEXT: "Name": "increment", // CHECK-NEXT: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "t", // CHECK-NEXT: "Type": "T" // CHECK-NEXT: } @@ -32,6 +34,7 @@ template Incrementable auto incrementTwo(T t); // CHECK-NEXT: "Template": { // CHECK-NEXT: "Constraints": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "Incrementable", // CHECK-NEXT: "Name": "Incrementable", // CHECK-NEXT: "Path": "", @@ -46,10 +49,13 @@ template Incrementable auto incrementTwo(T t); // CHECK-NEXT: "USR": "{{[0-9A-F]*}}" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "function", // CHECK-NEXT: "IsStatic": false, // CHECK-NEXT: "Name": "incrementTwo", // CHECK-NEXT: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "t", // CHECK-NEXT: "Type": "T" // CHECK-NEXT: } @@ -64,6 +70,7 @@ template Incrementable auto incrementTwo(T t); // CHECK-NEXT: "Template": { // CHECK-NEXT: "Constraints": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Expression": "Incrementable", // CHECK-NEXT: "Name": "Incrementable", // CHECK-NEXT: "Path": "", diff --git a/clang-tools-extra/test/clang-doc/json/method-template.cpp b/clang-tools-extra/test/clang-doc/json/method-template.cpp index ac8450a72d3a7..14232d00e277a 100644 --- a/clang-tools-extra/test/clang-doc/json/method-template.cpp +++ b/clang-tools-extra/test/clang-doc/json/method-template.cpp @@ -9,6 +9,7 @@ struct MyClass { // CHECK: "PublicFunctions": [ // CHECK-NEXT: { +// CHECK-NEXT: "InfoType": "function", // CHECK-NEXT: "IsStatic": false, // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}method-template.cpp", @@ -21,6 +22,7 @@ struct MyClass { // CHECK-NEXT: ], // CHECK-NEXT: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "param", // CHECK-NEXT: "Type": "T" // CHECK-NEXT: } diff --git a/clang-tools-extra/test/clang-doc/json/namespace.cpp b/clang-tools-extra/test/clang-doc/json/namespace.cpp index 779d7b49f5581..4b6b38869f714 100644 --- a/clang-tools-extra/test/clang-doc/json/namespace.cpp +++ b/clang-tools-extra/test/clang-doc/json/namespace.cpp @@ -20,8 +20,11 @@ enum Color { typedef int MyTypedef; // CHECK: { +// CHECK-NEXT: "DocumentationFileName": "index", // CHECK-NEXT: "Enums": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "enum", // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}namespace.cpp", // CHECK-NEXT: "LineNumber": 14 @@ -36,6 +39,7 @@ typedef int MyTypedef; // CHECK-NEXT: "Value": "1" // CHECK-NEXT: }, // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "BLUE", // CHECK-NEXT: "ValueExpr": "5" // CHECK-NEXT: } @@ -47,10 +51,13 @@ typedef int MyTypedef; // CHECK-NEXT: ], // CHECK-NEXT: "Functions": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "function", // CHECK-NEXT: "IsStatic": false, // CHECK-NEXT: "Name": "myFunction", // CHECK-NEXT: "Params": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "Param", // CHECK-NEXT: "Type": "int" // CHECK-NEXT: } @@ -65,9 +72,13 @@ typedef int MyTypedef; // CHECK-NEXT: "USR": "{{[0-9A-F]*}}" // CHECK-NEXT: } // CHECK-NEXT: ], +// CHECK-NEXT: "HasEnums": true, +// CHECK-NEXT: "HasRecords": true, +// CHECK-NEXT: "InfoType": "namespace", // CHECK-NEXT: "Name": "", // CHECK-NEXT: "Namespaces": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "NestedNamespace", // CHECK-NEXT: "Path": "", // CHECK-NEXT: "QualName": "NestedNamespace", @@ -76,6 +87,8 @@ typedef int MyTypedef; // CHECK-NEXT: ], // CHECK-NEXT: "Records": [ // CHECK-NEXT: { +// CHECK-NEXT: "DocumentationFileName": "_ZTV7MyClass", +// CHECK-NEXT: "End": true, // CHECK-NEXT: "Name": "MyClass", // CHECK-NEXT: "Path": "GlobalNamespace", // CHECK-NEXT: "QualName": "MyClass", @@ -84,6 +97,8 @@ typedef int MyTypedef; // CHECK-NEXT: ], // CHECK-NEXT: "Typedefs": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "typedef", // CHECK-NEXT: "IsUsing": false, // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}namespace.cpp", @@ -104,6 +119,8 @@ typedef int MyTypedef; // CHECK-NEXT: "USR": "0000000000000000000000000000000000000000" // CHECK-NEXT: "Variables": [ // CHECK-NEXT: { +// CHECK-NEXT: "End": true, +// CHECK-NEXT: "InfoType": "variable", // CHECK-NEXT: "IsStatic": true, // CHECK-NEXT: "Location": { // CHECK-NEXT: "Filename": "{{.*}}namespace.cpp", diff --git a/clang-tools-extra/test/clang-doc/json/nested-namespace.cpp b/clang-tools-extra/test/clang-doc/json/nested-namespace.cpp index 54f95c4d041ca..255e540bd6c7c 100644 --- a/clang-tools-extra/test/clang-doc/json/nested-namespace.cpp +++ b/clang-tools-extra/test/clang-doc/json/nested-namespace.cpp @@ -12,6 +12,8 @@ namespace nested { // NESTED: "Variables": [ // NESTED-NEXT: { +// NESTED-NEXT: "End": true, +// NESTED-NEXT: "InfoType": "variable", // NESTED-NEXT: "IsStatic": false, // NESTED-NEXT: "Location": { // NESTED-NEXT: "Filename": "{{.*}}nested-namespace.cpp", @@ -24,6 +26,8 @@ namespace nested { // INNER: "Variables": [ // INNER-NEXT: { +// INNER-NEXT: "End": true, +// INNER-NEXT: "InfoType": "variable", // INNER-NEXT: "IsStatic": false, // INNER-NEXT: "Location": { // INNER-NEXT: "Filename": "{{.*}}nested-namespace.cpp", diff --git a/clang-tools-extra/test/clang-doc/mustache-index.cpp b/clang-tools-extra/test/clang-doc/mustache-index.cpp index cad4cc8b6931a..910233b943666 100644 --- a/clang-tools-extra/test/clang-doc/mustache-index.cpp +++ b/clang-tools-extra/test/clang-doc/mustache-index.cpp @@ -1,6 +1,6 @@ // RUN: rm -rf %t && mkdir -p %t // RUN: clang-doc --format=mustache --output=%t --executor=standalone %s -// RUN: FileCheck %s < %t/GlobalNamespace/index.html +// RUN: FileCheck %s < %t/index.html enum Color { RED, @@ -15,7 +15,7 @@ class Foo; // CHECK-NEXT: // CHECK-NEXT:
    // CHECK-NEXT: // CHECK-NEXT:
// CHECK: