diff --git a/.clang-tidy b/.clang-tidy index c03da523..184cac3d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -4,6 +4,7 @@ Checks: > -abseil-*, -altera-*, -android-*, + -boost-*, -cert-*, -clang-analyzer-*, -cppcoreguidelines-*, @@ -17,11 +18,12 @@ Checks: > -objc-*, -openmp-*, -zircon-*, - -boost-use-ranges, -bugprone-branch-clone, -bugprone-easily-swappable-parameters, -bugprone-narrowing-conversions, -bugprone-switch-missing-default-case, + -bugprone-throwing-static-initialization, + -bugprone-unchecked-string-to-number-conversion, -concurrency-mt-unsafe, -misc-no-recursion, -misc-non-private-member-variables-in-classes, @@ -29,18 +31,15 @@ Checks: > -modernize-avoid-c-arrays, -modernize-loop-convert, -modernize-pass-by-value, - -modernize-return-braced-init-list, -modernize-use-auto, - -modernize-use-emplace, - -modernize-use-equals-default, - -modernize-use-equals-delete, - -modernize-use-default-member-init, + -modernize-use-designated-initializers, -modernize-use-nodiscard, - -modernize-use-override, + -modernize-use-ranges, + -modernize-use-starts-ends-with, -modernize-use-trailing-return-type, - -modernize-use-using, -readability-avoid-nested-conditional-operator, -readability-braces-around-statements, + -readability-container-contains, -readability-function-cognitive-complexity, -readability-function-size, -readability-implicit-bool-conversion, @@ -48,10 +47,9 @@ Checks: > -readability-isolate-declaration, -readability-magic-numbers, -readability-redundant-inline-specifier, - -readability-simplify-boolean-expr, + -readability-use-concise-preprocessor-directives, -readability-uppercase-literal-suffix, -performance-avoid-endl, - -performance-enum-size, -performance-inefficient-string-concatenation, -performance-no-automatic-move, -performance-noexcept-move-constructor diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..bd256eb3 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,16 @@ +# +# This file lists revisions that should be ignored when considering +# attribution for the actual code written. Code style changes should +# not be considered as modifications with regards to attribution. +# +# To see clean and meaningful blame information. +# $ git blame important.py --ignore-revs-file .git-blame-ignore-revs +# +# To configure git to automatically ignore revisions listed in a file +# on every call to git blame. +# $ git config blame.ignoreRevsFile .git-blame-ignore-revs +# +# Ignore changes introduced when doing global file format changes + +# Switch to uncrustify (#517) +cfd179711f413aa8e0da9c2f437ad4f8938d5f70 diff --git a/.github/workflows/CI-mingw.yml b/.github/workflows/CI-mingw.yml new file mode 100644 index 00000000..99991737 --- /dev/null +++ b/.github/workflows/CI-mingw.yml @@ -0,0 +1,137 @@ +name: CI-mingw + +on: [push, pull_request] + +permissions: + contents: read + +defaults: + run: + shell: msys2 {0} + +jobs: + build: + + strategy: + matrix: + compiler: [g++, clang++] + # TODO: add MSYS after #556 is fixed + # MINGW32 was removed since MSYS2 is phasing out 32-bit environments + msystem: [MINGW64, CLANG64] + include: + #- msystem: MSYS + # pkg-prefix: '' + - msystem: MINGW64 + pkg-prefix: 'mingw-w64-x86_64-' + - msystem: CLANG64 + pkg-prefix: 'mingw-w64-clang-x86_64-' + - compiler: g++ + compiler-pkg: gcc + - compiler: clang++ + compiler-pkg: clang + exclude: + - msystem: CLANG64 + compiler: g++ + fail-fast: false + + runs-on: windows-2025 + + env: + CXX: ${{ matrix.compiler }} + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up MSYS2 + uses: msys2/setup-msys2@v2 + with: + release: false # use pre-installed + msystem: ${{ matrix.msystem }} + # TODO: install mingw-w64-x86_64-make and use mingw32.make instead - currently fails with "Windows Subsystem for Linux has no installed distributions." + # TODO: also run tests with non-prefixed Python? + install: >- + make + ${{ matrix.pkg-prefix }}cmake + ${{ matrix.pkg-prefix }}python + ${{ matrix.pkg-prefix }}python-pytest + + - name: install compiler + run: | + pacman -S --noconfirm ${{ matrix.pkg-prefix }}${{ matrix.compiler-pkg }} + ${CXX} -v + + - name: make simplecpp + run: | + make -j$(nproc) CXXOPTS="-Werror" + + # gcc *and* clang are required to run-tests.py + # install it at this point since it has gcc as dependency which might interfere with the build + - name: install compiler (clang) + if: matrix.compiler == 'g++' + run: | + pacman -S --noconfirm clang + + - name: install compiler (gcc) + if: matrix.compiler == 'clang++' + run: | + pacman -S --noconfirm gcc + + - name: make test + run: | + # TODO: run tests with Windows paths + make -j$(nproc) test + + - name: selfcheck + run: | + # TODO: run tests with Windows paths + make -j$(nproc) selfcheck + + - name: make (c++14) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++14" + + - name: make (c++17) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++17" + + - name: make (c++20) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++20" + + - name: make (c++23) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++23" + + - name: Run CMake + run: | + cmake -S . -B cmake.output -DCMAKE_COMPILE_WARNING_AS_ERROR=On + + - name: CMake simplecpp + run: | + cmake --build cmake.output --target simplecpp -- -j $(nproc) + + - name: CMake testrunner + run: | + cmake --build cmake.output --target testrunner -- -j $(nproc) + + - name: Run testrunner + run: | + ./cmake.output/testrunner + + - name: Run with libstdc++ debug mode + if: matrix.compiler == 'g++' + run: | + make clean + make -j$(nproc) test selfcheck CXXOPTS="-Werror -g3 -D_GLIBCXX_DEBUG" + + - name: Run with libc++ hardening mode + if: matrix.compiler == 'clang++' && matrix.msystem == 'CLANG64' + run: | + make clean + make -j$(nproc) test selfcheck CXXOPTS="-Werror -stdlib=libc++ -g3 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG" LDOPTS="-lc++" diff --git a/.github/workflows/CI-unixish.yml b/.github/workflows/CI-unixish.yml index 60361389..0f459601 100644 --- a/.github/workflows/CI-unixish.yml +++ b/.github/workflows/CI-unixish.yml @@ -10,13 +10,17 @@ jobs: strategy: matrix: - os: [ubuntu-22.04, ubuntu-24.04, macos-13, macos-14, macos-15] + os: [ubuntu-22.04, ubuntu-22.04-arm, ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel, macos-26, macos-26-intel] compiler: [clang++] include: - os: ubuntu-22.04 compiler: g++ + - os: ubuntu-22.04-arm + compiler: g++ - os: ubuntu-24.04 compiler: g++ + - os: ubuntu-24.04-arm + compiler: g++ fail-fast: false runs-on: ${{ matrix.os }} @@ -25,26 +29,36 @@ jobs: CXX: ${{ matrix.compiler }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false + # the man-db trigger causes package installations to stall for several minutes at times. so just drop the package. + # see https://github.com/actions/runner/issues/4030 + - name: Remove man-db package on ubuntu + if: matrix.os == 'ubuntu-24.04' + run: | + sudo apt-get update + sudo apt-get remove man-db + - name: Install missing software on ubuntu if: matrix.os == 'ubuntu-24.04' run: | sudo apt-get update sudo apt-get install valgrind + # llvm contains llvm-profdata - name: Install missing software on ubuntu (clang++) - if: matrix.os == 'ubuntu-24.04' && matrix.compiler == 'clang++' + if: contains(matrix.os, 'ubuntu') && matrix.compiler == 'clang++' run: | sudo apt-get update - sudo apt-get install libc++-18-dev + sudo apt-get install libc++-dev llvm + # coreutils contains "nproc" - name: Install missing software on macos if: contains(matrix.os, 'macos') run: | - brew install python3 + brew install coreutils - name: Install missing Python packages run: | @@ -61,9 +75,30 @@ jobs: run: | make -j$(nproc) selfcheck + - name: make (c++14) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++14" + + - name: make (c++17) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++17" + + - name: make (c++20) + run: | + make clean + make -j$(nproc) CXXOPTS="-Werror -std=c++20" + + - name: make (c++23) + run: | + make clean + # ubuntu-22.04 do not support c++23 yet + make -j$(nproc) CXXOPTS="-Werror -std=c++2b" + - name: Run CMake run: | - cmake -S . -B cmake.output -DCMAKE_COMPILE_WARNING_AS_ERROR=On + cmake -S . -B cmake.output -Werror=dev --warn-uninitialized -DCMAKE_COMPILE_WARNING_AS_ERROR=On - name: CMake simplecpp run: | @@ -73,14 +108,18 @@ jobs: run: | cmake --build cmake.output --target testrunner -- -j $(nproc) ./cmake.output/testrunner + # Re-run tests from within the build directory to validate that + # SIMPLECPP_TEST_SOURCE_DIR is correctly defined and resolved + (cd cmake.output && ./testrunner) - name: Run valgrind if: matrix.os == 'ubuntu-24.04' run: | make clean - make -j$(nproc) + make -j$(nproc) CXXOPTS="-O1" valgrind --leak-check=full --num-callers=50 --show-reachable=yes --track-origins=yes --gen-suppressions=all --error-exitcode=42 ./testrunner - valgrind --leak-check=full --num-callers=50 --show-reachable=yes --track-origins=yes --gen-suppressions=all --error-exitcode=42 ./simplecpp simplecpp.cpp -e + # TODO: run Python tests with valgrind + VALGRIND_TOOL=memcheck ./selfcheck.sh - name: Run with libstdc++ debug mode if: matrix.os == 'ubuntu-24.04' && matrix.compiler == 'g++' @@ -95,7 +134,7 @@ jobs: make -j$(nproc) test selfcheck CXXOPTS="-Werror -stdlib=libc++ -g3 -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG" LDOPTS="-lc++" - name: Run AddressSanitizer - if: matrix.os == 'ubuntu-24.04' + if: matrix.os == 'ubuntu-24.04' || matrix.os == 'macos-26' run: | make clean make -j$(nproc) test selfcheck CXXOPTS="-Werror -O2 -g3 -fsanitize=address" LDOPTS="-fsanitize=address" @@ -103,7 +142,7 @@ jobs: ASAN_OPTIONS: detect_stack_use_after_return=1 - name: Run UndefinedBehaviorSanitizer - if: matrix.os == 'ubuntu-24.04' + if: matrix.os == 'ubuntu-24.04' || matrix.os == 'macos-26' run: | make clean make -j$(nproc) test selfcheck CXXOPTS="-Werror -O2 -g3 -fsanitize=undefined -fno-sanitize=signed-integer-overflow" LDOPTS="-fsanitize=undefined -fno-sanitize=signed-integer-overflow" @@ -116,3 +155,44 @@ jobs: run: | make clean make -j$(nproc) test selfcheck CXXOPTS="-Werror -O2 -g3 -stdlib=libc++ -fsanitize=memory" LDOPTS="-lc++ -fsanitize=memory" + + - name: Run callgrind + if: matrix.os == 'ubuntu-24.04' + run: | + wget https://github.com/danmar/simplecpp/archive/refs/tags/1.5.1.tar.gz + tar xvf 1.5.1.tar.gz + rm -f 1.5.1.tar.gz + + make clean + make -j$(nproc) CXXOPTS="-O2 -g3" simplecpp + VALGRIND_TOOL=callgrind SIMPLECPP_PATH=simplecpp-1.5.1 ./selfcheck.sh >callgrind.log || (cat callgrind.log && false) + cat callgrind.log + + # PGO - start + make clean + make -j$(nproc) CXXOPTS="-O2 -g3 -fprofile-generate" LDOPTS="-fprofile-generate" simplecpp + SIMPLECPP_PATH=simplecpp-1.5.1 ./selfcheck.sh >/dev/null + + if compgen -G "default_*.profraw" > /dev/null; then + llvm-profdata merge -output=default.profdata default_*.profraw + fi + + make clean + make -j$(nproc) CXXOPTS="-O2 -g3 -fprofile-use" LDOPTS="-fprofile-use" simplecpp + VALGRIND_TOOL=callgrind SIMPLECPP_PATH=simplecpp-1.5.1 ./selfcheck.sh >callgrind_pgo.log || (cat callgrind_pgo.log && false) + cat callgrind_pgo.log + # PGO - end + + for f in callgrind.out.*; + do + callgrind_annotate --auto=no $f > $f.annotated.log + head -50 $f.annotated.log + done + rm -rf simplecpp-1.5.1 + + - uses: actions/upload-artifact@v4 + if: matrix.os == 'ubuntu-24.04' + with: + name: Callgrind Output - ${{ matrix.compiler }} + path: | + ./callgrind.* diff --git a/.github/workflows/CI-windows.yml b/.github/workflows/CI-windows.yml index 767bba6c..e5c4e4b4 100644 --- a/.github/workflows/CI-windows.yml +++ b/.github/workflows/CI-windows.yml @@ -12,30 +12,37 @@ permissions: defaults: run: shell: cmd - + jobs: build: strategy: matrix: - os: [windows-2022, windows-2025] + os: [windows-2022, windows-2025, windows-11-arm] config: [Release, Debug] + include: + - os: windows-2022 + sln: sln + - os: windows-2025 + sln: slnx + - os: windows-11-arm + sln: slnx fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false - name: Setup msbuild.exe uses: microsoft/setup-msbuild@v2 - - name: Set up Python 3.13 - uses: actions/setup-python@v5 + - name: Set up Python + uses: actions/setup-python@v6 with: - python-version: '3.13' + python-version: '3.14' check-latest: true - name: Install missing Python packages @@ -45,12 +52,12 @@ jobs: - name: Run CMake run: | - cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_COMPILE_WARNING_AS_ERROR=On . || exit /b !errorlevel! + cmake -A x64 -Werror=dev --warn-uninitialized -DCMAKE_COMPILE_WARNING_AS_ERROR=On . || exit /b !errorlevel! - name: Build run: | - msbuild -m simplecpp.sln /p:Configuration=${{ matrix.config }} /p:Platform=x64 || exit /b !errorlevel! - + msbuild -m simplecpp.${{ matrix.sln }} /p:Configuration=${{ matrix.config }} /p:Platform=x64 || exit /b !errorlevel! + - name: Test run: | .\${{ matrix.config }}\testrunner.exe || exit /b !errorlevel! @@ -63,4 +70,20 @@ jobs: run: | set SIMPLECPP_EXE_PATH=.\${{ matrix.config }}\simplecpp.exe python -m pytest integration_test.py -vv || exit /b !errorlevel! - + + - name: Run CMake (c++17) + run: | + cmake -S . -B build.cxx17 -A x64 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=17 -DCMAKE_COMPILE_WARNING_AS_ERROR=On || exit /b !errorlevel! + + - name: Build (c++17) + run: | + msbuild -m build.cxx17\simplecpp.${{ matrix.sln }} /p:Configuration=${{ matrix.config }} /p:Platform=x64 || exit /b !errorlevel! + + - name: Run CMake (c++20) + run: | + cmake -S . -B build.cxx20 -A x64 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=20 -DCMAKE_COMPILE_WARNING_AS_ERROR=On || exit /b !errorlevel! + + - name: Build (c++20) + run: | + msbuild -m build.cxx20\simplecpp.${{ matrix.sln }} /p:Configuration=${{ matrix.config }} /p:Platform=x64 || exit /b !errorlevel! + diff --git a/.github/workflows/clang-tidy.yml b/.github/workflows/clang-tidy.yml index fd71bdfe..2819fb9d 100644 --- a/.github/workflows/clang-tidy.yml +++ b/.github/workflows/clang-tidy.yml @@ -13,10 +13,17 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: persist-credentials: false + # the man-db trigger causes package installations to stall for several minutes at times. so just drop the package. + # see https://github.com/actions/runner/issues/4030 + - name: Remove man-db package + run: | + sudo apt-get update + sudo apt-get remove man-db + - name: Install missing software run: | sudo apt-get update @@ -26,19 +33,69 @@ jobs: run: | wget https://apt.llvm.org/llvm.sh chmod +x llvm.sh - sudo ./llvm.sh 20 - sudo apt-get install clang-tidy-20 + sudo ./llvm.sh 22 + sudo apt-get install clang-tidy-22 - name: Verify clang-tidy configuration run: | - clang-tidy-20 --verify-config + clang-tidy-22 --verify-config + + - name: Prepare CMake (C++11) + run: | + cmake -S . -B cmake.output.cxx11 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=11 -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + env: + CXX: clang-22 + + - name: Clang-Tidy (C++11) + run: | + run-clang-tidy-22 -q -j $(nproc) -enable-check-profile -p=cmake.output.cxx11 + + - name: Prepare CMake (C++14) + run: | + cmake -S . -B cmake.output.cxx14 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=14 -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + env: + CXX: clang-22 + + - name: Clang-Tidy (C++14) + run: | + run-clang-tidy-22 -q -j $(nproc) -enable-check-profile -p=cmake.output.cxx14 + + - name: Prepare CMake (C++17) + run: | + cmake -S . -B cmake.output.cxx17 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=17 -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + env: + CXX: clang-22 + + - name: Clang-Tidy (C++17) + run: | + run-clang-tidy-22 -q -j $(nproc) -enable-check-profile -p=cmake.output.cxx17 + + - name: Prepare CMake (C++20) + run: | + cmake -S . -B cmake.output.cxx20 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=20 -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + env: + CXX: clang-22 + + - name: Clang-Tidy (C++20) + run: | + run-clang-tidy-22 -q -j $(nproc) -enable-check-profile -p=cmake.output.cxx20 + + - name: Prepare CMake (C++23) + run: | + cmake -S . -B cmake.output.cxx23 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=23 -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + env: + CXX: clang-22 + + - name: Clang-Tidy (C++23) + run: | + run-clang-tidy-22 -q -j $(nproc) -enable-check-profile -p=cmake.output.cxx23 - - name: Prepare CMake + - name: Prepare CMake (C++26) run: | - cmake -S . -B cmake.output -G "Unix Makefiles" -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + cmake -S . -B cmake.output.cxx26 -Werror=dev --warn-uninitialized -DCMAKE_CXX_STANDARD=26 -DCMAKE_COMPILE_WARNING_AS_ERROR=On -DCMAKE_EXPORT_COMPILE_COMMANDS=ON env: - CXX: clang-20 + CXX: clang-22 - - name: Clang-Tidy + - name: Clang-Tidy (C++26) run: | - run-clang-tidy-20 -q -j $(nproc) -p=cmake.output + run-clang-tidy-22 -q -j $(nproc) -enable-check-profile -p=cmake.output.cxx26 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 00000000..e9f1361d --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,62 @@ +# Syntax reference https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions +# Environment reference https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners +name: format + +on: + push: + branches: + - 'master' + - 'releases/**' + - '1.*' + tags: + - '1.*' + pull_request: + +permissions: + contents: read + +jobs: + format: + + runs-on: ubuntu-22.04 + + defaults: + run: + shell: bash -euo pipefail {0} + + env: + UNCRUSTIFY_INSTALL_DIR: ${{ github.workspace }}/runformat-uncrustify + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Determine uncrustify version + id: get-uncrustify-version + run: | + version="$(./runformat --expected-uncrustify-version)" + echo "Expected uncrustify version: $version" + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Set UNCRUSTIFY_VERSION env variable + run: | + version=$(./runformat --expected-uncrustify-version) + echo "version [$version]" + echo "UNCRUSTIFY_VERSION=${version}" >> "$GITHUB_ENV" + + - name: Cache uncrustify + uses: actions/cache@v4 + id: cache-uncrustify + with: + path: ${{ env.UNCRUSTIFY_INSTALL_DIR }} + key: ${{ runner.os }}-uncrustify-${{ steps.get-uncrustify-version.outputs.version }} + + - name: Install uncrustify + if: steps.cache-uncrustify.outputs.cache-hit != 'true' + run: | + ./runformat --install --install-dir "${UNCRUSTIFY_INSTALL_DIR}" + + - name: Uncrustify check + run: | + ./runformat diff --git a/.github/workflows/gcc-versions.yml b/.github/workflows/gcc-versions.yml new file mode 100644 index 00000000..9c365a24 --- /dev/null +++ b/.github/workflows/gcc-versions.yml @@ -0,0 +1,30 @@ +# the purpose of this github action is to test that simplecpp source code can be compiled using old gcc versions. +# the gcc images we use here are copied from the official gcc images on docker hub + +name: gcc-versions + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + build: + + strategy: + matrix: + #image: ["ghcr.io/cppcheck-opensource/gcc:5.4", "ghcr.io/cppcheck-opensource/gcc:6.5", "ghcr.io/cppcheck-opensource/gcc:7.5", "ghcr.io/cppcheck-opensource/gcc:8.5", "ghcr.io/cppcheck-opensource/gcc:9.5"] + image: ["ghcr.io/cppcheck-opensource/gcc:5.4"] + fail-fast: false + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Build and run tests + run: | + docker run --rm -v ${{ github.workspace }}:/simplecpp -w /simplecpp ${{ matrix.image }} make -j$(nproc) simplecpp testrunner + docker run --rm -v ${{ github.workspace }}:/simplecpp -w /simplecpp ${{ matrix.image }} ./testrunner diff --git a/.gitignore b/.gitignore index 113cf360..34d9c55d 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ *.app simplecpp testrunner +/.runformat-uncrustify # CLion /.idea diff --git a/.uncrustify.cfg b/.uncrustify.cfg new file mode 100644 index 00000000..81722ff7 --- /dev/null +++ b/.uncrustify.cfg @@ -0,0 +1,170 @@ +# Uncrustify-0.80.1_f + +# The original size of tabs in the input. +# +# Default: 8 +input_tab_size = 4 # unsigned number + +# The size of tabs in the output (only used if align_with_tabs=true). +# +# Default: 8 +output_tab_size = 4 # unsigned number + +# Add or remove space between 'while' and '('. +sp_while_paren_open = add # ignore/add/remove/force + +# Add or remove space around boolean operators '&&' and '||'. +sp_bool = force # ignore/add/remove/force + +# Add or remove space inside '(' and ')'. +sp_inside_paren = remove # ignore/add/remove/force + +# Add or remove space between nested parentheses, i.e. '((' vs. ') )'. +sp_paren_paren = remove # ignore/add/remove/force + +# Add or remove space between ')' and '{'. +sp_paren_brace = force # ignore/add/remove/force + +# Add or remove space between pointer stars '*'. +sp_between_ptr_star = remove # ignore/add/remove/force + +# Add or remove space before '<'. +sp_before_angle = remove # ignore/add/remove/force + +# Add or remove space inside '<' and '>'. +sp_inside_angle = remove # ignore/add/remove/force + +# Add or remove space after '>'. +sp_after_angle = add # ignore/add/remove/force + +# Add or remove space between '>' and '(' as found in 'new List(foo);'. +sp_angle_paren = remove # ignore/add/remove/force + +# Add or remove space between '>' and a word as in 'List m;' or +# 'template static ...'. +sp_angle_word = add # ignore/add/remove/force + +# Add or remove space between '>' and '>' in '>>' (template stuff). +# +# Default: add +sp_angle_shift = ignore # ignore/add/remove/force + +# (C++11) Permit removal of the space between '>>' in 'foo >'. Note +# that sp_angle_shift cannot remove the space without this option. +sp_permit_cpp11_shift = true # true/false + +# Add or remove space before '(' of control statements ('if', 'for', 'switch', +# 'while', etc.). +sp_before_sparen = force # ignore/add/remove/force + +# Add or remove space inside '(' and ')' of control statements. +sp_inside_sparen = remove # ignore/add/remove/force + +# Add or remove space after ')' of control statements. +sp_after_sparen = force # ignore/add/remove/force + +# Add or remove space between ')' and '{' of of control statements. +sp_sparen_brace = force # ignore/add/remove/force + +# Add or remove space before ';' in non-empty 'for' statements. +sp_before_semi_for = remove # ignore/add/remove/force + +# Add or remove space after the final semicolon of an empty part of a for +# statement, as in 'for ( ; ; )'. +sp_after_semi_for_empty = remove # ignore/add/remove/force + +# Add or remove space before '[]'. +sp_before_squares = remove # ignore/add/remove/force + +# Add or remove space before C++17 structured bindings. +sp_cpp_before_struct_binding = ignore # ignore/add/remove/force + +# Add or remove space inside a non-empty '[' and ']'. +sp_inside_square = remove # ignore/add/remove/force + +# Add or remove space after class ':'. +sp_after_class_colon = force # ignore/add/remove/force + +# Add or remove space before class ':'. +sp_before_class_colon = force # ignore/add/remove/force + +# Add or remove space inside '{}'. +sp_inside_braces_empty = remove # ignore/add/remove/force + +# Add or remove space between 'else' and '{' if on the same line. +sp_else_brace = force # ignore/add/remove/force + +# Add or remove space between '}' and 'else' if on the same line. +sp_brace_else = force # ignore/add/remove/force + +# Add or remove space before the '{' of a 'catch' statement, if the '{' and +# 'catch' are on the same line, as in 'catch (decl) {'. +sp_catch_brace = force # ignore/add/remove/force + +# Add or remove space between '}' and 'catch' if on the same line. +sp_brace_catch = force # ignore/add/remove/force + +# The number of columns to indent per level. Usually 2, 3, 4, or 8. +# +# Default: 8 +indent_columns = 4 # unsigned number + +# How to use tabs when indenting code. +# +# 0: Spaces only +# 1: Indent with tabs to brace level, align with spaces (default) +# 2: Indent and align with tabs, using spaces when not on a tabstop +# +# Default: 1 +indent_with_tabs = 0 # unsigned number + +# Whether to indent the body of a 'namespace'. +indent_namespace = true # true/false + +# Whether the 'class' body is indented. +indent_class = true # true/false + +# How to indent access specifiers that are followed by a +# colon. +# +# >0: Absolute column where 1 is the leftmost column +# <=0: Subtract from brace indent +# +# Default: 1 +indent_access_spec = -4 # number + +# Whether to collapse empty blocks between '{' and '}' except for functions. +# Use nl_collapse_empty_body_functions to specify how empty function braces +# should be formatted. +nl_collapse_empty_body = true # true/false + +# Whether to collapse empty blocks between '{' and '}' for functions only. +# If true, overrides nl_inside_empty_func. +nl_collapse_empty_body_functions = true # true/false + +# Whether to convert all tabs to spaces in comments. If false, tabs in +# comments are left alone, unless used for indenting. +cmt_convert_tab_to_spaces = true # true/false + +# An offset value that controls the indentation of the body of a multiline #define. +# 'body' refers to all the lines of a multiline #define except the first line. +# Requires 'pp_ignore_define_body = false'. +# +# <0: Absolute column: the body indentation starts off at the specified column +# (ex. -3 ==> the body is indented starting from column 3) +# >=0: Relative to the column of the '#' of '#define' +# (ex. 3 ==> the body is indented starting 3 columns at the right of '#') +# +# Default: 8 +pp_multiline_define_body_indent = 4 # number + +# The value might be used twice: +# - at the assignment +# - at the opening brace +# +# To prevent the double use of the indentation value, use this option with the +# value 'true'. +# +# true: indentation will be used only once +# false: indentation will be used every time (default) +indent_cpp_lambda_only_once = true # true/false diff --git a/CMakeLists.txt b/CMakeLists.txt index f9e3eb6d..220c6c3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,9 +1,13 @@ cmake_minimum_required (VERSION 3.10) project (simplecpp LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD 11 CACHE STRING "C++ standard to use") set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(CMAKE_CXX_STANDARD LESS 11) + message(FATAL_ERROR "C++ standard was set to ${CMAKE_CXX_STANDARD} but 11 is required as a minimum") +endif() + include(CheckCXXCompilerFlag) if (WIN32) @@ -19,46 +23,74 @@ function(add_compile_options_safe FLAG) endif() endfunction() -if (CMAKE_CXX_COMPILER_ID MATCHES "GNU") +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") add_compile_options(-pedantic) + add_compile_options(-Wall) add_compile_options(-Wextra) add_compile_options(-Wcast-qual) # Cast for removing type qualifiers add_compile_options(-Wfloat-equal) # Floating values used in equality comparisons add_compile_options(-Wmissing-declarations) # If a global function is defined without a previous declaration add_compile_options(-Wmissing-format-attribute) # - add_compile_options(-Wno-long-long) add_compile_options(-Wpacked) # add_compile_options(-Wredundant-decls) # if anything is declared more than once in the same scope add_compile_options(-Wundef) - add_compile_options(-Wno-missing-braces) - add_compile_options(-Wno-sign-compare) - add_compile_options(-Wno-multichar) add_compile_options(-Woverloaded-virtual) # when a function declaration hides virtual functions from a base class add_compile_options(-Wsuggest-attribute=noreturn) - add_compile_options_safe(-Wuseless-cast) -elseif (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + if (NOT MINGW) + add_compile_options_safe(-Wuseless-cast) + endif() + + # we are not interested in these + set_source_files_properties(test.cpp PROPERTIES COMPILE_FLAGS -Wno-multichar) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") add_compile_definitions(_CRT_SECURE_NO_WARNINGS) - # TODO: bump warning level - #add_compile_options(/W4) # Warning Level - # TODO: enable warning + + add_compile_options(/W4) # Warning Level + + add_compile_options(/wd4127) # warning C4127: conditional expression is constant + add_compile_options(/wd4244) # warning C4244: 'x': conversion from 'int' to 'char', possible loss of data add_compile_options(/wd4267) # warning C4267: '...': conversion from 'size_t' to 'unsigned int', possible loss of data + add_compile_options(/wd4706) # warning C4706: assignment within conditional expression elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Weverything) + # no need for c++98 compatibility add_compile_options(-Wno-c++98-compat-pedantic) - # these are not really fixable - add_compile_options(-Wno-exit-time-destructors -Wno-global-constructors -Wno-weak-vtables) + + # these are not really fixable until newer standards + add_compile_options(-Wno-exit-time-destructors) + add_compile_options(-Wno-global-constructors) + add_compile_options(-Wno-weak-vtables) add_compile_options_safe(-Wno-unsafe-buffer-usage) - # we are not interested in these - add_compile_options(-Wno-multichar -Wno-four-char-constants) - # ignore C++11-specific warning - add_compile_options(-Wno-suggest-override -Wno-suggest-destructor-override) + add_compile_options_safe(-Wno-nrvo) + # contradicts -Wcovered-switch-default add_compile_options(-Wno-switch-default) + if (MINGW) + add_compile_options(-Wno-reserved-macro-identifier) + add_compile_options(-Wno-unused-macros) + endif() + + # these are experimental warnings which might produce false positives + add_compile_options_safe(-Wno-thread-safety-negative) + add_compile_options_safe(-Wno-thread-safety-beta) + + # TODO: add [[clang::lifetimebound]] annotations? + add_compile_options_safe(-Wno-lifetime-safety-intra-tu-suggestions) + add_compile_options_safe(-Wno-lifetime-safety-intra-tu-constructor-suggestions) + add_compile_options_safe(-Wno-lifetime-safety-cross-tu-constructor-suggestions) + # TODO: fix these? - add_compile_options(-Wno-padded -Wno-sign-conversion -Wno-implicit-int-conversion -Wno-shorten-64-to-32 -Wno-shadow-field-in-constructor) + add_compile_options(-Wno-padded) + add_compile_options(-Wno-sign-conversion) + add_compile_options(-Wno-implicit-int-conversion) + add_compile_options(-Wno-shorten-64-to-32) + add_compile_options(-Wno-shadow-field-in-constructor) + + # we are not interested in these + set_source_files_properties(test.cpp PROPERTIES COMPILE_FLAGS "-Wno-multichar -Wno-four-char-constants") if (CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 14 OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 14) # TODO: verify this regression still exists in clang-15 @@ -70,12 +102,21 @@ elseif (CMAKE_CXX_COMPILER_ID MATCHES "Clang") # use force DWARF 4 debug format since not all tools might be able to handle DWARF 5 yet - e.g. valgrind on ubuntu 20.04 add_compile_options(-gdwarf-4) endif() + if (APPLE) + # CMake is sometimes chosing the wrong compiler on macos-* runners + # see https://github.com/actions/runner/issues/4034 + add_compile_options(-Wno-poison-system-directories) + endif() endif() add_library(simplecpp_obj OBJECT simplecpp.cpp) add_executable(simplecpp $ main.cpp) add_executable(testrunner $ test.cpp) +target_compile_definitions(testrunner + PRIVATE + SIMPLECPP_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}" +) enable_testing() add_test(NAME testrunner COMMAND testrunner) diff --git a/Makefile b/Makefile index d899d2cd..b7b54597 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,18 @@ all: testrunner simplecpp -CXXFLAGS = -Wall -Wextra -pedantic -Wcast-qual -Wfloat-equal -Wmissing-declarations -Wmissing-format-attribute -Wredundant-decls -Wundef -Wno-multichar -Wold-style-cast -std=c++11 -g $(CXXOPTS) +CPPFLAGS ?= +CXXFLAGS = -Wall -Wextra -pedantic -Wcast-qual -Wfloat-equal -Wmissing-declarations -Wmissing-format-attribute -Wpacked -Wredundant-decls -Wundef -Woverloaded-virtual -std=c++11 -g $(CXXOPTS) LDFLAGS = -g $(LDOPTS) -%.o: %.cpp simplecpp.h - $(CXX) $(CXXFLAGS) -c $< +# Define test source dir macro for compilation (preprocessor flags) +TEST_CPPFLAGS = -DSIMPLECPP_TEST_SOURCE_DIR=\"$(CURDIR)\" + +# Only test.o gets the define +test.o: CPPFLAGS += $(TEST_CPPFLAGS) +test.o: CXXFLAGS += -Wno-multichar +%.o: %.cpp simplecpp.h + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< testrunner: test.o simplecpp.o $(CXX) $(LDFLAGS) simplecpp.o test.o -o testrunner @@ -16,7 +23,7 @@ test: testrunner simplecpp python3 -m pytest integration_test.py -vv selfcheck: simplecpp - ./selfcheck.sh + CXX=$(CXX) ./selfcheck.sh simplecpp: main.o simplecpp.o $(CXX) $(LDFLAGS) main.o simplecpp.o -o simplecpp diff --git a/README.md b/README.md index e4bdb2f1..dae242ea 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Simple C/C++ preprocessor +NOTE: This repository was recently moved from https://github.com/danmar/simplecpp to https://github.com/cppcheck-opensource/simplecpp. Please make sure to update all your references to it accordingly. + This is a simple C/C++ preprocessor. The goal is to have good conformance with the C and C++ standards and to handle nonstandard preprocessor extensions in gcc / clang / visual studio preprocessors. Most of the preprocessor testcases in gcc and clang are handled OK by simplecpp. diff --git a/integration_test.py b/integration_test.py index a59ae338..b24f6352 100644 --- a/integration_test.py +++ b/integration_test.py @@ -17,7 +17,7 @@ def __test_relative_header_create_header(dir, with_pragma_once=True): #error header_was_already_included #endif const int dummy = 1; - """) + """'\n') return header_file, "error: #error header_was_already_included" def __test_relative_header_create_source(dir, include1, include2, is_include1_sys=False, is_include2_sys=False, inv=False): @@ -31,7 +31,7 @@ def __test_relative_header_create_source(dir, include1, include2, is_include1_sy #undef TEST_H_INCLUDED #include {format_include(include1, is_include1_sys)} #include {format_include(include2, is_include2_sys)} - """) + """'\n') return src_file @pytest.mark.parametrize("with_pragma_once", (False, True)) @@ -201,27 +201,27 @@ def test_same_name_header(record_property, tmpdir): #include #include TEST - """) + """'\n') with open(header_a, "wt") as f: f.write(""" #include "same_name.h" - """) + """'\n') with open(header_b, "wt") as f: f.write(""" #include "same_name.h" - """) + """'\n') with open(same_name_a, "wt") as f: f.write(""" #define TEST E - """) + """'\n') with open(same_name_b, "wt") as f: f.write(""" #define TEST OK - """) + """'\n') args = [ format_include_path_arg(include_a), @@ -279,13 +279,13 @@ def test_pragma_once_matching(record_property, tmpdir): for n in names_to_test: f.write(f""" #include {n} - """); + """'\n'); with open(once_header, "wt") as f: f.write(f""" #pragma once ONCE - """); + """'\n'); args = [ format_include_path_arg(test_dir), @@ -298,3 +298,221 @@ def test_pragma_once_matching(record_property, tmpdir): assert stdout.count("ONCE") == 1 assert stderr == "" + + +def test_input_multiple(record_property, tmpdir): + test_file = os.path.join(tmpdir, "test.c") + with open(test_file, 'w'): + pass + + test_file_1 = os.path.join(tmpdir, "test1.c") + with open(test_file_1, 'w'): + pass + + args = [ + 'test.c', + 'test1.c' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: multiple filenames specified\n" == stdout + + +def test_input_missing(record_property, tmpdir): + args = [ + 'missing.c' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: could not open file 'missing.c'\n" == stdout + + +def test_input_dir(record_property, tmpdir): + test_dir = os.path.join(tmpdir, "test") + os.mkdir(test_dir) + + args = [ + 'test' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: could not open file 'test'\n" == stdout + + +def test_incpath_missing(record_property, tmpdir): + test_file = os.path.join(tmpdir, "test.c") + with open(test_file, 'w'): + pass + + test_dir = os.path.join(tmpdir, "test") + os.mkdir(test_dir) + + args = [ + '-Itest', + '-Imissing', + 'test.c' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: could not find include path 'missing'\n" == stdout + + +def test_incpath_file(record_property, tmpdir): + test_file = os.path.join(tmpdir, "test.c") + with open(test_file, 'w'): + pass + + inc_dir = os.path.join(tmpdir, "inc") + os.mkdir(inc_dir) + + inc_file = os.path.join(tmpdir, "inc.h") + with open(test_file, 'w'): + pass + + args = [ + '-Iinc', + '-Iinc.h', + 'test.c' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: could not find include path 'inc.h'\n" == stdout + + +def test_incfile_missing(record_property, tmpdir): + test_file = os.path.join(tmpdir, "test.c") + with open(test_file, 'w'): + pass + + inc_file = os.path.join(tmpdir, "inc.h") + with open(inc_file, 'w'): + pass + + args = [ + '-include=inc.h', + '-include=missing.h', + 'test.c' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: could not open include 'missing.h'\n" == stdout + + +def test_incpath_dir(record_property, tmpdir): + test_file = os.path.join(tmpdir, "test.c") + with open(test_file, 'w'): + pass + + inc_file = os.path.join(tmpdir, "inc.h") + with open(inc_file, 'w'): + pass + + inc_dir = os.path.join(tmpdir, "inc") + os.mkdir(inc_dir) + + args = [ + '-include=inc.h', + '-include=inc', + 'test.c' + ] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert '' == stderr + assert "error: could not open include 'inc'\n" == stdout + + +def test_include_header_twice(tmpdir): + """ Issue #581 - Failure when header is included twice with different + macros defined""" + + header_file = tmpdir / 'test.h' + with open(header_file, 'wt') as f: + f.write(f""" + #if defined AAA + #elif defined BBB + # undef BBB + #endif + + #ifdef BBB + # error BBB is defined + #endif + """'\n') + + test_file = os.path.join(tmpdir, 'test.c') + with open(test_file, 'wt') as f: + f.write(f""" + # define Y + # include "test.h" + + # define BBB + # include "test.h" + """'\n') + + args = [test_file] + + _, stdout, stderr = simplecpp(args, cwd=tmpdir) + + assert stderr == '' + + +def test_define(record_property, tmpdir): # #589 + test_file = os.path.join(tmpdir, "test.cpp") + with open(test_file, 'w') as f: + f.write( +"""#define PREFIX_WITH_MACRO(test_name) Macro##test_name + +TEST_P(PREFIX_WITH_MACRO(NamingTest), n) {} +""") + + args = [ + '-DTEST_P(A,B)=void __ ## A ## _ ## B ( )', + 'test.cpp' + ] + + exitcode, stdout, stderr = simplecpp(args, cwd=tmpdir) + record_property("stdout", stdout) + record_property("stderr", stderr) + + assert exitcode == 0 + assert stderr == "test.cpp:1: syntax error: failed to expand 'TEST_P', Invalid ## usage when expanding 'TEST_P': Combining ')' and '_' yields an invalid token.\n" + assert stdout == '\n' + +def test_utf16_bom(tmpdir): + test_file = os.path.join(tmpdir, "test.cpp") + with open(test_file, 'wb') as f: + f.write(b'\xFF\xFE\x3B\x00\x0A\x00') + + args = [test_file] + + exitcode, stdout, stderr = simplecpp(args, cwd=tmpdir) + + assert exitcode == 0 + assert stderr == '' + assert stdout == ';\n' diff --git a/main.cpp b/main.cpp index a6d14386..ec0fc5c7 100644 --- a/main.cpp +++ b/main.cpp @@ -3,24 +3,45 @@ * Copyright (C) 2016-2023 simplecpp team */ +#define SIMPLECPP_TOKENLIST_ALLOW_PTR 0 #include "simplecpp.h" +#include #include #include #include #include +#include #include +#include +#include #include +static bool isDir(const std::string& path) +{ + struct stat file_stat; + if (stat(path.c_str(), &file_stat) == -1) + return false; + + return (file_stat.st_mode & S_IFMT) == S_IFDIR; +} + int main(int argc, char **argv) { bool error = false; const char *filename = nullptr; - bool use_istream = false; + enum : std::uint8_t { + File, + Fstream, + Sstream, + CharBuffer + } toklist_inf = File; bool fail_on_error = false; + bool linenrs = false; // Settings.. simplecpp::DUI dui; + dui.removeComments = true; bool quiet = false; bool error_only = false; for (int i = 1; i < argc; i++) { @@ -30,49 +51,107 @@ int main(int argc, char **argv) const char c = arg[1]; switch (c) { case 'D': { // define symbol - const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; - dui.defines.push_back(value); found = true; + const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; + if (!value) { + std::cout << "error: option -D with no value." << std::endl; + error = true; + break; + } + dui.defines.emplace_back(value); break; } case 'U': { // undefine symbol + found = true; const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; + if (!value) { + std::cout << "error: option -U with no value." << std::endl; + error = true; + break; + } dui.undefined.insert(value); - found = true; break; } case 'I': { // include path - const char * const value = arg[2] ? (argv[i] + 2) : argv[++i]; - dui.includePaths.push_back(value); found = true; + const char * const value = arg[2] ? (arg + 2) : argv[++i]; + if (!value) { + std::cout << "error: option -I with no value." << std::endl; + error = true; + break; + } + dui.includePaths.emplace_back(value); break; } case 'i': if (std::strncmp(arg, "-include=",9)==0) { - dui.includes.push_back(arg+9); found = true; - } else if (std::strncmp(arg, "-is",3)==0) { - use_istream = true; + std::string value = arg + 9; + if (value.empty()) { + std::cout << "error: option -include with no value." << std::endl; + error = true; + break; + } + dui.includes.emplace_back(std::move(value)); + } + else if (std::strncmp(arg, "-input=",7)==0) { found = true; + const std::string input = arg + 7; + if (input.empty()) { + std::cout << "error: option -input with no value." << std::endl; + error = true; + break; + } + if (input == "file") { + toklist_inf = File; + } else if (input == "fstream") { + toklist_inf = Fstream; + } else if (input == "sstream") { + toklist_inf = Sstream; + } else if (input == "buffer") { + toklist_inf = CharBuffer; + } else { + std::cout << "error: unknown input type '" << input << "'." << std::endl; + error = true; + break; + } } break; case 's': if (std::strncmp(arg, "-std=",5)==0) { - dui.std = arg + 5; found = true; + std::string value = arg + 5; + if (value.empty()) { + std::cout << "error: option -std with no value." << std::endl; + error = true; + break; + } + dui.std = std::move(value); } break; case 'q': - quiet = true; - found = true; + if (std::strcmp(arg, "-q")==0) { + found = true; + quiet = true; + } break; case 'e': - error_only = true; - found = true; + if (std::strcmp(arg, "-e")==0) { + found = true; + error_only = true; + } break; case 'f': - fail_on_error = true; - found = true; + if (std::strcmp(arg, "-f")==0) { + fail_on_error = true; + found = true; + } + break; + case 'l': + if (std::strcmp(arg, "-l")==0) { + linenrs = true; + found = true; + } break; } if (!found) { @@ -81,18 +160,18 @@ int main(int argc, char **argv) } } else if (filename) { std::cout << "error: multiple filenames specified" << std::endl; - std::exit(1); + return 1; } else { filename = arg; } } if (error) - std::exit(1); + return 1; if (quiet && error_only) { std::cout << "error: -e cannot be used in conjunction with -q" << std::endl; - std::exit(1); + return 1; } if (!filename) { @@ -104,42 +183,80 @@ int main(int argc, char **argv) std::cout << " -UNAME Undefine NAME." << std::endl; std::cout << " -std=STD Specify standard." << std::endl; std::cout << " -q Quiet mode (no output)." << std::endl; - std::cout << " -is Use std::istream interface." << std::endl; + std::cout << " -input=INPUT Specify input type - file (default), fstream, sstream, buffer." << std::endl; std::cout << " -e Output errors only." << std::endl; - std::exit(0); + std::cout << " -f Fail when errors were encountered (exitcode 1)." << std::endl; + std::cout << " -l Print lines numbers." << std::endl; + return 0; } - dui.removeComments = true; + // TODO: move this logic into simplecpp + bool inp_missing = false; + + for (const std::string& inc : dui.includes) { + std::ifstream f(inc); + if (!f.is_open() || isDir(inc)) { + inp_missing = true; + std::cout << "error: could not open include '" << inc << "'" << std::endl; + } + } + + for (const std::string& inc : dui.includePaths) { + if (!isDir(inc)) { + inp_missing = true; + std::cout << "error: could not find include path '" << inc << "'" << std::endl; + } + } + + std::ifstream f(filename); + if (!f.is_open() || isDir(filename)) { + inp_missing = true; + std::cout << "error: could not open file '" << filename << "'" << std::endl; + } + + if (inp_missing) + return 1; // Perform preprocessing simplecpp::OutputList outputList; std::vector files; - simplecpp::TokenList *rawtokens; - if (use_istream) { - std::ifstream f(filename); - if (!f.is_open()) { - std::cout << "error: could not open file '" << filename << "'" << std::endl; - std::exit(1); + simplecpp::TokenList outputTokens(files); + { + simplecpp::TokenList *rawtokens; + if (toklist_inf == Fstream) { + rawtokens = new simplecpp::TokenList(f,files,filename,dui,&outputList); } - rawtokens = new simplecpp::TokenList(f, files,filename,&outputList); - } else { - rawtokens = new simplecpp::TokenList(filename,files,&outputList); + else if (toklist_inf == Sstream || toklist_inf == CharBuffer) { + std::ostringstream oss; + oss << f.rdbuf(); + f.close(); + const std::string s = oss.str(); + if (toklist_inf == Sstream) { + std::istringstream iss(s); + rawtokens = new simplecpp::TokenList(iss,files,filename,dui,&outputList); + } + else { + rawtokens = new simplecpp::TokenList({s.data(),s.size()},files,filename,dui,&outputList); + } + } else { + f.close(); + rawtokens = new simplecpp::TokenList(filename,files,dui,&outputList); + } + if (dui.removeComments) + rawtokens->removeComments(); + simplecpp::FileDataCache filedata; + simplecpp::preprocess(outputTokens, *rawtokens, files, filedata, dui, &outputList); + simplecpp::cleanup(filedata); + delete rawtokens; } - rawtokens->removeComments(); - simplecpp::TokenList outputTokens(files); - simplecpp::FileDataCache filedata; - simplecpp::preprocess(outputTokens, *rawtokens, files, filedata, dui, &outputList); - simplecpp::cleanup(filedata); - delete rawtokens; - rawtokens = nullptr; // Output if (!quiet) { if (!error_only) - std::cout << outputTokens.stringify() << std::endl; + std::cout << outputTokens.stringify(linenrs) << std::endl; for (const simplecpp::Output &output : outputList) { - std::cerr << output.location.file() << ':' << output.location.line << ": "; + std::cerr << outputTokens.file(output.location) << ':' << output.location.line << ": "; switch (output.type) { case simplecpp::Output::ERROR: std::cerr << "#error: "; @@ -156,7 +273,13 @@ int main(int argc, char **argv) case simplecpp::Output::SYNTAX_ERROR: std::cerr << "syntax error: "; break; + case simplecpp::Output::DIRECTIVE_AS_MACRO_PARAMETER: + std::cerr << "directive as macro parameter: "; + break; case simplecpp::Output::PORTABILITY_BACKSLASH: + case simplecpp::Output::PORTABILITY_LINE_DIRECTIVE: + case simplecpp::Output::PORTABILITY_NO_EOF_NEWLINE: + case simplecpp::Output::PORTABILITY_STRINGIFY: std::cerr << "portability: "; break; case simplecpp::Output::UNHANDLED_CHAR_ERROR: diff --git a/run-tests.py b/run-tests.py index 5017f4c1..e3b0eeae 100644 --- a/run-tests.py +++ b/run-tests.py @@ -1,17 +1,30 @@ - import glob import os +import shutil import subprocess import sys -def cleanup(out): - ret = '' - for s in out.decode('utf-8').split('\n'): - if len(s) > 1 and s[0] == '#': + +def cleanup(out: str) -> str: + parts = [] + for line in out.splitlines(): + if len(line) > 1 and line[0] == '#': continue - s = "".join(s.split()) - ret = ret + s - return ret + parts.append("".join(line.split())) + return "".join(parts) + + +# Check for required compilers and exit if any are missing +CLANG_EXE = shutil.which('clang') +if not CLANG_EXE: + sys.exit('Failed to run tests: clang compiler not found') + +GCC_EXE = shutil.which('gcc') +if not GCC_EXE: + sys.exit('Failed to run tests: gcc compiler not found') + +SIMPLECPP_EXE = './simplecpp' + commands = [] @@ -67,8 +80,6 @@ def cleanup(out): 'macro_rescan_varargs.c', # todo, high priority - 'c99-6_10_3_4_p5.c', - 'c99-6_10_3_4_p6.c', 'expr_usual_conversions.c', # condition is true: 4U - 30 >= 0 'stdint.c', @@ -78,6 +89,21 @@ def cleanup(out): 'pr57580.c', ] + +def run(compiler_executable: str, compiler_args: list[str]) -> tuple[int, str, str, str]: + """Execute a compiler command and capture its exit code, stdout, and stderr.""" + compiler_cmd = [compiler_executable] + compiler_cmd.extend(compiler_args) + + with subprocess.Popen(compiler_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="utf-8") as process: + stdout, stderr = process.communicate() + exit_code = process.returncode + + output = cleanup(stdout) + error = (stderr or "").strip() + return (exit_code, output, stdout, error) + + numberOfSkipped = 0 numberOfFailed = 0 numberOfFixed = 0 @@ -89,34 +115,32 @@ def cleanup(out): numberOfSkipped = numberOfSkipped + 1 continue - clang_cmd = ['clang'] - clang_cmd.extend(cmd.split(' ')) - p = subprocess.Popen(clang_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - comm = p.communicate() - clang_output = cleanup(comm[0]) + _, clang_output_c, clang_output, _ = run(CLANG_EXE, cmd.split(' ')) - gcc_cmd = ['gcc'] - gcc_cmd.extend(cmd.split(' ')) - p = subprocess.Popen(gcc_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - comm = p.communicate() - gcc_output = cleanup(comm[0]) + _, gcc_output_c, gcc_output, _ = run(GCC_EXE, cmd.split(' ')) - simplecpp_cmd = ['./simplecpp'] # -E is not supported and we bail out on unknown options - simplecpp_cmd.extend(cmd.replace('-E ', '', 1).split(' ')) - p = subprocess.Popen(simplecpp_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - comm = p.communicate() - simplecpp_ec = p.returncode - simplecpp_output = cleanup(comm[0]) - simplecpp_err = comm[0].decode('utf-8').strip() - - if simplecpp_output != clang_output and simplecpp_output != gcc_output: + simplecpp_ec, simplecpp_output_c, simplecpp_output, simplecpp_err = run(SIMPLECPP_EXE, cmd.replace('-E ', '', 1).split(' ')) + + if simplecpp_output_c != clang_output_c and simplecpp_output_c != gcc_output_c: filename = cmd[cmd.rfind('/')+1:] if filename in todo: print('TODO ' + cmd) usedTodos.append(filename) else: print('FAILED ' + cmd) + print('---expected (clang):') + print(clang_output_c) + print('---expected (gcc):') + print(gcc_output_c) + print('---actual:') + print(simplecpp_output_c) + print('---output (clang):') + print(clang_output) + print('---output (gcc):') + print(gcc_output) + print('---output (simplecpp):') + print(simplecpp_output) if simplecpp_ec: print('simplecpp failed - ' + simplecpp_err) numberOfFailed = numberOfFailed + 1 diff --git a/runastyle b/runastyle deleted file mode 100755 index 64298273..00000000 --- a/runastyle +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# The version check in this script is used to avoid commit battles -# between different developers that use different astyle versions as -# different versions might have different output (this has happened in -# the past). - -# If project management wishes to take a newer astyle version into use -# just change this string to match the start of astyle version string. -ASTYLE_VERSION="Artistic Style Version 3.0.1" -ASTYLE="astyle" - -DETECTED_VERSION=`$ASTYLE --version 2>&1` -if [[ "$DETECTED_VERSION" != ${ASTYLE_VERSION}* ]]; then - echo "You should use: ${ASTYLE_VERSION}"; - echo "Detected: ${DETECTED_VERSION}" - exit 1; -fi - -style="--style=kr --indent=spaces=4 --indent-namespaces --lineend=linux --min-conditional-indent=0" -options="--options=none --pad-header --unpad-paren --suffix=none --convert-tabs --attach-inlines --attach-classes --attach-namespaces" - -$ASTYLE $style $options *.cpp -$ASTYLE $style $options *.h diff --git a/runastyle.bat b/runastyle.bat deleted file mode 100644 index b8f11561..00000000 --- a/runastyle.bat +++ /dev/null @@ -1,26 +0,0 @@ -@REM Script to run AStyle on the sources -@REM The version check in this script is used to avoid commit battles -@REM between different developers that use different astyle versions as -@REM different versions might have different output (this has happened in -@REM the past). - -@REM If project management wishes to take a newer astyle version into use -@REM just change this string to match the start of astyle version string. -@SET ASTYLE_VERSION="Artistic Style Version 3.0.1" -@SET ASTYLE="astyle" - -@SET DETECTED_VERSION="" -@FOR /F "tokens=*" %%i IN ('%ASTYLE% --version') DO SET DETECTED_VERSION=%%i -@ECHO %DETECTED_VERSION% | FINDSTR /B /C:%ASTYLE_VERSION% > nul && ( - ECHO "%DETECTED_VERSION%" matches %ASTYLE_VERSION% -) || ( - ECHO You should use: %ASTYLE_VERSION% - ECHO Detected: "%DETECTED_VERSION%" - GOTO EXIT_ERROR -) - -@SET STYLE=--style=kr --indent=spaces=4 --indent-namespaces --lineend=linux --min-conditional-indent=0 -@SET OPTIONS=--pad-header --unpad-paren --suffix=none --convert-tabs --attach-inlines --attach-classes --attach-namespaces - -%ASTYLE% %STYLE% %OPTIONS% *.cpp -%ASTYLE% %STYLE% %OPTIONS% *.h \ No newline at end of file diff --git a/runformat b/runformat new file mode 100755 index 00000000..0dc1ad52 --- /dev/null +++ b/runformat @@ -0,0 +1,176 @@ +#!/bin/bash +# +# runformat - format this project's C++ sources with Uncrustify. +# +# Usage: +# ./runformat # format using the configured Uncrustify +# ./runformat --install # download, build, and use Uncrustify locally +# ./runformat --install --install-dir /abs/path +# ./runformat --expected-uncrustify-version # print ONLY the expected Uncrustify version +# +# You may also set: +# UNCRUSTIFY=/abs/path/to/uncrustify # use a specific binary +# UNCRUSTIFY_INSTALL_DIR=/abs/path # where `--install` will install +# +# Requirements: +# - All developers must use the *exact* same Uncrustify version to avoid format churn. +# - Either: +# * Have `uncrustify` in PATH, or +# * Set env var UNCRUSTIFY=/absolute/path/to/uncrustify, or +# * Run `./runformat --install` to fetch & build the pinned version locally. +# +# Notes: +# - The local install lives under: ./.runformat-uncrustify/uncrustify--install +# - The config file is expected at: ./.uncrustify.cfg +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +UNCRUSTIFY_VERSION="0.80.1" +UNCRUSTIFY_HASH="6bf662e05c4140dd4df5e45d6690cad96b4ef23c293b85813f5c725bbf1894d0" + +UNCRUSTIFY_WORK_DIR="${SCRIPT_DIR}/.runformat-uncrustify" + +# Allow external install dir override (arg or env). If not set, default under work dir. +DEFAULT_INSTALL_DIR="${UNCRUSTIFY_WORK_DIR}/uncrustify-${UNCRUSTIFY_VERSION}-install" +UNCRUSTIFY_INSTALL_DIR="${UNCRUSTIFY_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}" +UNCRUSTIFY_BIN="${UNCRUSTIFY_INSTALL_DIR}/bin/uncrustify" + +# Allow override via env; default to local pinned build path. +UNCRUSTIFY="${UNCRUSTIFY:-$UNCRUSTIFY_BIN}" +UNCRUSTIFY_CONFIG="${SCRIPT_DIR}/.uncrustify.cfg" + +err() { echo -e >&2 "ERROR: $@\n"; } +die() { err $@; exit 1; } + +install_uncrustify() { + local root="uncrustify-${UNCRUSTIFY_VERSION}" + local file="${root}.tar.gz" + local url="https://github.com/uncrustify/uncrustify/releases/download/${root}/${file}" + + mkdir -p "${UNCRUSTIFY_WORK_DIR}" + + echo "Downloading ${file}..." + curl -fsSL -o "${UNCRUSTIFY_WORK_DIR}/${file}" "${url}" + + ( + cd "${UNCRUSTIFY_WORK_DIR}" + + echo "${UNCRUSTIFY_HASH} ${file}" > "${file}.sha256" + sha256sum -c "${file}.sha256" + rm -f "${file}.sha256" + + command -v cmake >/dev/null 2>&1 || die "cmake executable not found." + + echo "Extracting archive..." + rm -rf "${root}" "${root}-build" + mkdir -p "${root}" + tar -xzf "${file}" --strip-components=1 -C "${root}" + + echo "Configuring (prefix: ${UNCRUSTIFY_INSTALL_DIR})..." + cmake \ + -DCMAKE_BUILD_TYPE:STRING=Release \ + -DCMAKE_INSTALL_PREFIX:PATH="${UNCRUSTIFY_INSTALL_DIR}" \ + -S "${root}" -B "${root}-build" + + echo "Building & installing..." + cmake --build "${root}-build" --config Release --target install --parallel + ) + + echo "Installed Uncrustify to: ${UNCRUSTIFY_INSTALL_DIR}" +} + +print_usage_and_exit() { + sed -n '2,25p' "$0" | sed 's/^# \{0,1\}//' + exit 0 +} + +# Print ONLY expected Uncrustify version (no extra text). +print_expected_uncrustify_version_and_exit() { + printf '%s\n' "$UNCRUSTIFY_VERSION" + exit 0 +} + +# -------------------------- +# Argument parsing +# -------------------------- +DO_INSTALL=0 +PRINT_EXPECTED_UNCRUSTIFY_VERSION=0 +# Accept: --install, --install-dir , -h/--help +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + print_usage_and_exit + ;; + --install) + DO_INSTALL=1 + shift + ;; + --install-dir) + [[ $# -ge 2 ]] || die "$1 requires a path argument" + UNCRUSTIFY_INSTALL_DIR="$(readlink -m "$2" 2>/dev/null || realpath -m "$2")" + UNCRUSTIFY_BIN="${UNCRUSTIFY_INSTALL_DIR}/bin/uncrustify" + # Only update UNCRUSTIFY default if user hasn't explicitly set it + if [[ "${UNCRUSTIFY:-}" != "${UNCRUSTIFY_BIN}" ]]; then + UNCRUSTIFY="${UNCRUSTIFY_BIN}" + fi + shift 2 + ;; + --expected-uncrustify-version) + PRINT_EXPECTED_UNCRUSTIFY_VERSION=1 + shift + ;; + *) + # ignore unrecognized positional args for now + shift + ;; + esac +done + +if [[ "$DO_INSTALL" -eq 1 ]]; then + install_uncrustify + # Ensure we use the freshly installed binary for this run + UNCRUSTIFY="$UNCRUSTIFY_BIN" +fi + +# If requested, print ONLY the expected Uncrustify version and exit. +if [[ "$PRINT_EXPECTED_UNCRUSTIFY_VERSION" -eq 1 ]]; then + print_expected_uncrustify_version_and_exit +fi + +# -------------------------- +# Validate & run +# -------------------------- + +# Check Uncrustify availability +if ! command -v "$UNCRUSTIFY" >/dev/null 2>&1; then + err "Uncrustify executable not found: $UNCRUSTIFY" + die "Add it to PATH, set UNCRUSTIFY=/path/to/uncrustify, or run: $0 --install [--install-dir DIR]" +fi + +# Version check +DETECTED_VERSION="$("$UNCRUSTIFY" --version 2>&1 | grep -oE '[0-9]+(\.[0-9]+)*' | head -n1 || true)" +echo "Detected Uncrustify: ${DETECTED_VERSION:-unknown}" +if [[ "$DETECTED_VERSION" != "${UNCRUSTIFY_VERSION}" ]]; then + die "Expected Uncrustify ${UNCRUSTIFY_VERSION}. Re-run with --install (and optionally --install-dir) or set UNCRUSTIFY." +fi + +# Config check +[[ -f "$UNCRUSTIFY_CONFIG" ]] || die "Uncrustify config not found at: $UNCRUSTIFY_CONFIG" + +# Run formatter +echo "Running formatter..." +$UNCRUSTIFY -c "$UNCRUSTIFY_CONFIG" -l CPP --no-backup --replace *.cpp *.h + +# Show diff and fail if changes exist +echo "Checking for formatting changes..." +git diff --exit-code || { + echo + echo "Formatting changes were applied. Please review and commit." + exit 1 +} + +echo "Formatting is clean." diff --git a/selfcheck.sh b/selfcheck.sh index a43ef8c5..b2129cc9 100755 --- a/selfcheck.sh +++ b/selfcheck.sh @@ -1,11 +1,144 @@ -#!/bin/sh +#!/bin/bash -output=$(./simplecpp simplecpp.cpp -e -f 2>&1) +if [ -z "$SIMPLECPP_PATH" ]; then + SIMPLECPP_PATH=. +fi + +if [ -n "$VALGRIND_TOOL" ]; then + if [ "$VALGRIND_TOOL" = "memcheck" ]; then + VALGRIND_OPTS="--error-limit=yes --leak-check=full --num-callers=50 --show-reachable=yes --track-origins=yes --gen-suppressions=all --error-exitcode=42" + elif [ "$VALGRIND_TOOL" = "callgrind" ]; then + VALGRIND_OPTS="--tool=callgrind" + else + echo "unsupported valgrind tool '$VALGRIND_TOOL'" + exit 1 + fi + VALGRIND_CMD="valgrind --tool=$VALGRIND_TOOL --log-fd=9 $VALGRIND_OPTS" + VALGRIND_REDIRECT="valgrind_$VALGRIND_TOOL.log" +else + VALGRIND_CMD= + VALGRIND_REDIRECT="/dev/null" +fi + +output=$($VALGRIND_CMD ./simplecpp "$SIMPLECPP_PATH/simplecpp.cpp" -e -f 2>&1 9> "$VALGRIND_REDIRECT") ec=$? +cat "$VALGRIND_REDIRECT" errors=$(echo "$output" | grep -v 'Header not found: <') if [ $ec -ne 0 ]; then - # only fail if got errors which do not refer to missing system includes + # only fail if we got errors which do not refer to missing system includes if [ ! -z "$errors" ]; then exit $ec fi -fi \ No newline at end of file +fi + +if [ -z "$CXX" ]; then + exit 0 +fi + +cxx_type=$($CXX --version | head -1 | cut -d' ' -f1) +if [ "$cxx_type" = "Ubuntu" ] || [ "$cxx_type" = "Debian" ]; then + cxx_type=$($CXX --version | head -1 | cut -d' ' -f2) +fi + +# TODO: generate defines from compiler +if [ "$cxx_type" = "g++" ] || [ "$cxx_type" = "g++.exe" ]; then + defs= + defs="$defs -D__GNUC__" + defs="$defs -D__STDC__" + defs="$defs -D__x86_64__" + defs="$defs -D__STDC_HOSTED__" + defs="$defs -D__CHAR_BIT__=8" + if [ "${MSYSTEM}" = "MINGW32" ] || [ "${MSYSTEM}" = "MINGW64" ]; then + defs="$defs -D_WIN32" + fi + defs="$defs -D__has_builtin(x)=(1)" + defs="$defs -D__has_cpp_attribute(x)=(1)" + defs="$defs -D__has_attribute(x)=(1)" + defs="$defs -Ddefined(x)=(0)" + + inc= + while read line + do + inc="$inc -I$line" + done <<< "$($CXX -x c++ -v -c -S - 2>&1 < /dev/null | grep -e'^ [/A-Z]' | grep -v /cc1plus)" +elif [ "$cxx_type" = "clang" ]; then + # libstdc++ + defs= + defs="$defs -D__x86_64__" + defs="$defs -D__STDC_HOSTED__" + defs="$defs -D__CHAR_BIT__=8" + defs="$defs -D__BYTE_ORDER__=1234" + defs="$defs -D__SIZEOF_SIZE_T__=8" + if [ "${MSYSTEM}" = "MINGW32" ] || [ "${MSYSTEM}" = "MINGW64" ] || [ "${MSYSTEM}" = "CLANG64" ]; then + defs="$defs -D_WIN32" + fi + defs="$defs -D__has_builtin(x)=(1)" + defs="$defs -D__has_cpp_attribute(x)=(1)" + defs="$defs -D__has_feature(x)=(1)" + defs="$defs -D__has_include_next(x)=(1)" + defs="$defs -D__has_attribute(x)=(0)" + defs="$defs -D__building_module(x)=(0)" + defs="$defs -D__has_extension(x)=(1)" + defs="$defs -Ddefined(x)=(0)" + + inc= + while read line + do + inc="$inc -I$line" + done <<< "$($CXX -x c++ -v -c -S - 2>&1 < /dev/null | grep -e'^ [/A-Z]')" + + # TODO: enable + # libc++ + #defs= + #defs="$defs -D__x86_64__" + #defs="$defs -D__linux__" + #defs="$defs -D__SIZEOF_SIZE_T__=8" + #defs="$defs -D__has_include_next(x)=(0)" + #defs="$defs -D__has_builtin(x)=(1)" + #defs="$defs -D__has_feature(x)=(1)" + + #inc= + #while read line + #do + # inc="$inc -I$line" + #done <<< "$($CXX -x c++ -stdlib=libc++ -v -c -S - 2>&1 < /dev/null | grep -e'^ [/A-Z]')" +elif [ "$cxx_type" = "Apple" ]; then + defs= + defs="$defs -D__BYTE_ORDER__" + defs="$defs -D__APPLE__" + defs="$defs -D__GNUC__=15" + defs="$defs -D__x86_64__" + defs="$defs -D__SIZEOF_SIZE_T__=8" + defs="$defs -D__LITTLE_ENDIAN__" + defs="$defs -D__has_feature(x)=(0)" + defs="$defs -D__has_extension(x)=(1)" + defs="$defs -D__has_attribute(x)=(0)" + defs="$defs -D__has_cpp_attribute(x)=(0)" + defs="$defs -D__has_include_next(x)=(0)" + defs="$defs -D__has_builtin(x)=(1)" + defs="$defs -D__is_target_os(x)=(0)" + defs="$defs -D__is_target_arch(x)=(0)" + defs="$defs -D__is_target_vendor(x)=(0)" + defs="$defs -D__is_target_environment(x)=(0)" + defs="$defs -D__is_target_variant_os(x)=(0)" + defs="$defs -D__is_target_variant_environment(x)=(0)" + + inc= + while read line + do + inc="$inc -I$line" + # TODO: pass the framework path as such when possible + done <<< "$($CXX -x c++ -v -c -S - 2>&1 < /dev/null | grep -e'^ [/A-Z]' | sed 's/ (framework directory)//g')" + echo $inc +else + echo "unknown compiler '$cxx_type'" + exit 1 +fi + +# run with -std=gnuc++* so __has_include(...) is available +$VALGRIND_CMD ./simplecpp "$SIMPLECPP_PATH/simplecpp.cpp" -e -f -std=gnu++11 $defs $inc 9> "$VALGRIND_REDIRECT" +ec=$? +cat "$VALGRIND_REDIRECT" +if [ $ec -ne 0 ]; then + exit $ec +fi diff --git a/simplecpp.cpp b/simplecpp.cpp index fd327549..2223d431 100644 --- a/simplecpp.cpp +++ b/simplecpp.cpp @@ -3,25 +3,30 @@ * Copyright (C) 2016-2023 simplecpp team */ -#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) -# define _WIN32_WINNT 0x0602 -# define NOMINMAX -# include -# undef ERROR +// needs to be specified here otherwise _mingw.h will define it as 0x0601 +// causing FileIdInfo not to be available +#if defined(_WIN32) +# ifndef _WIN32_WINNT +# define _WIN32_WINNT 0x0602 +# endif #endif #include "simplecpp.h" +#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) +# define SIMPLECPP_WINDOWS +#endif + #include #include #include #include #include // IWYU pragma: keep +#include #include #include #include #include -#include #include #include #include @@ -40,10 +45,19 @@ #include #include -#ifdef _WIN32 +#if defined(_WIN32) +# ifndef NOMINMAX +# define NOMINMAX +# endif +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +# undef ERROR # include #else # include +# include #endif static bool isHex(const std::string &s) @@ -56,14 +70,12 @@ static bool isOct(const std::string &s) return s.size()>1 && (s[0]=='0') && (s[1] >= '0') && (s[1] < '8'); } -// TODO: added an undercore since this conflicts with a function of the same name in utils.h from Cppcheck source when building Cppcheck with MSBuild -static bool isStringLiteral_(const std::string &s) +static bool isStringLiteral(const std::string &s) { return s.size() > 1 && (s[0]=='\"') && (*s.rbegin()=='\"'); } -// TODO: added an undercore since this conflicts with a function of the same name in utils.h from Cppcheck source when building Cppcheck with MSBuild -static bool isCharLiteral_(const std::string &s) +static bool isCharLiteral(const std::string &s) { // char literal patterns can include 'a', '\t', '\000', '\xff', 'abcd', and maybe '' // This only checks for the surrounding '' but doesn't parse the content. @@ -84,6 +96,8 @@ static const simplecpp::TokenString IFNDEF("ifndef"); static const simplecpp::TokenString DEFINED("defined"); static const simplecpp::TokenString ELSE("else"); static const simplecpp::TokenString ELIF("elif"); +static const simplecpp::TokenString ELIFDEF("elifdef"); +static const simplecpp::TokenString ELIFNDEF("elifndef"); static const simplecpp::TokenString ENDIF("endif"); static const simplecpp::TokenString PRAGMA("pragma"); @@ -146,21 +160,69 @@ static bool sameline(const simplecpp::Token *tok1, const simplecpp::Token *tok2) return tok1 && tok2 && tok1->location.sameline(tok2->location); } +/** is s a punctuator (C11 6.4.6, C++ [lex.operators]) */ +static bool isPunctuator(const std::string &s) +{ + static const std::set punctuators{ + "[", "]", "(", ")", "{", "}", ".", "->", "++", "--", "&", "*", "+", "-", "~", "!", + "/", "%", "<<", ">>", "<", ">", "<=", ">=", "==", "!=", "^", "|", "&&", "||", + "?", ":", ";", "...", "=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|=", + ",", "#", "##", "<:", ":>", "<%", "%>", "%:", "%:%:", "::", ".*", "->*", "<=>" + }; + return punctuators.find(s) != punctuators.end(); +} + +/** is s a valid result when a punctuator is pasted with ## */ +static bool isValidPunctuatorPaste(const std::string &s) +{ + // a pp-number can start with '.' + return isPunctuator(s) || (s.size() >= 2 && s[0] == '.' && std::isdigit(static_cast(s[1]))); +} + +/* + * When an operation can't be evaluated (division by zero), its result is an + * error value. The error is only reported if the error value is used, so an + * operand that is not evaluated, as in "0 && (1/0)", does not cause an error. + */ +static const std::string ERROR_VALUE("#error:"); + +static bool isErrorValue(const simplecpp::Token *tok) +{ + return tok && tok->str().compare(0, ERROR_VALUE.size(), ERROR_VALUE) == 0; +} + +static bool isValue(const simplecpp::Token *tok) +{ + return tok && (tok->number || isErrorValue(tok)); +} + static bool isAlternativeBinaryOp(const simplecpp::Token *tok, const std::string &alt) { return (tok->name && tok->str() == alt && tok->previous && tok->next && - (tok->previous->number || tok->previous->name || tok->previous->op == ')') && - (tok->next->number || tok->next->name || tok->next->op == '(')); + (isValue(tok->previous) || tok->previous->name || tok->previous->op == ')') && + (isValue(tok->next) || tok->next->name || tok->next->op == '(')); +} + +static bool isAlternativeOpName(const simplecpp::Token *tok) +{ + static const std::set altop{"and","or","bitand","bitor","compl","not","not_eq","xor"}; + return tok->name && altop.find(tok->str()) != altop.end(); } static bool isAlternativeUnaryOp(const simplecpp::Token *tok, const std::string &alt) { - return ((tok->name && tok->str() == alt) && - (!tok->previous || tok->previous->op == '(') && - (tok->next && (tok->next->name || tok->next->number))); + if (!tok->name || tok->str() != alt) + return false; + // a unary operator comes first or after another operator, but not after an operand + const simplecpp::Token * const prev = tok->previous; + if (prev && (isValue(prev) || prev->op == ')' || (prev->name && !isAlternativeOpName(prev)))) + return false; + // it is followed by the start of an operand + const simplecpp::Token * const next = tok->next; + return next && (next->name || isValue(next) || next->isOneOf("(!~+-")); } static std::string replaceAll(std::string s, const std::string& from, const std::string& to) @@ -170,8 +232,6 @@ static std::string replaceAll(std::string s, const std::string& from, const std: return s; } -const std::string simplecpp::Location::emptyFileName; - void simplecpp::Location::adjust(const std::string &str) { if (strpbrk(str.c_str(), "\r\n") == nullptr) { @@ -233,68 +293,63 @@ void simplecpp::Token::printOut() const // cppcheck-suppress noConstructor - we call init() in the inherited to initialize the private members class simplecpp::TokenList::Stream { public: - virtual ~Stream() {} - - virtual int get() = 0; - virtual int peek() = 0; - virtual void unget() = 0; - virtual bool good() = 0; + virtual ~Stream() = default; + /** + * Read the next character. CR and CR LF are returned as LF. + * At the end of the input, EOF converted to unsigned char is returned + * and good() becomes false. + */ unsigned char readChar() { - unsigned char ch = static_cast(get()); - - // For UTF-16 encoded files the BOM is 0xfeff/0xfffe. If the - // character is non-ASCII character then replace it with 0xff - if (isUtf16) { - const unsigned char ch2 = static_cast(get()); - const int ch16 = makeUtf16Char(ch, ch2); - ch = static_cast(((ch16 >= 0x80) ? 0xff : ch16)); + int ch = getUnit(); + if (ch == EOF) { + atEnd = true; + lastChar = EOF; + return static_cast(EOF); } // Handling of newlines.. if (ch == '\r') { ch = '\n'; - - int ch2 = get(); - if (isUtf16) { - const int c2 = get(); - ch2 = makeUtf16Char(ch2, c2); - } - - if (ch2 != '\n') - ungetChar(); + const int ch2 = getUnit(); + if (ch2 != '\n' && ch2 != EOF) + buffer.push_back(static_cast(ch2)); } - return ch; + lastChar = ch; + return static_cast(ch); } - unsigned char peekChar() { - unsigned char ch = static_cast(peek()); - - // For UTF-16 encoded files the BOM is 0xfeff/0xfffe. If the - // character is non-ASCII character then replace it with 0xff - if (isUtf16) { - (void)get(); - const unsigned char ch2 = static_cast(peek()); - unget(); - const int ch16 = makeUtf16Char(ch, ch2); - ch = static_cast(((ch16 >= 0x80) ? 0xff : ch16)); - } - - // Handling of newlines.. - if (ch == '\r') - ch = '\n'; - - return ch; + /** The next character, without reading it. EOF at the end of the input. */ + int peekChar() { + const int ch = getUnit(); + if (ch == EOF) + return EOF; + buffer.push_back(static_cast(ch)); + return (ch == '\r') ? '\n' : ch; } + /** Put back the character that was last read with readChar() */ void ungetChar() { - unget(); - if (isUtf16) - unget(); + if (lastChar != EOF) + buffer.push_back(static_cast(lastChar)); + lastChar = EOF; + } + + /** + * false when readChar() has reached the end of the input. + * Unlike std::istream::good(), peekChar() at the end of the input does not make it false. + */ + bool good() const { + return !atEnd; } protected: + /** Raw input, used only by the Stream class. Call readChar(), peekChar() and ungetChar() instead. */ + virtual int get() = 0; + virtual int peek() = 0; + virtual void unget() = 0; + void init() { // initialize since we use peek() in getAndSkipBOM() isUtf16 = false; @@ -303,6 +358,38 @@ class simplecpp::TokenList::Stream { } private: + /** + * Read the next character from the input, or take it from the buffer. + * Newlines are not converted. + */ + int getUnit() { + if (!buffer.empty()) { + const auto ch = static_cast(buffer.back()); + buffer.pop_back(); + return ch; + } + if (inputEnd) + return EOF; + const int ch = get(); + if (ch == EOF) { + inputEnd = true; + return EOF; + } + if (!isUtf16) + return ch; + + // For UTF-16 encoded files the BOM is 0xfeff/0xfffe. If the + // character is non-ASCII character then replace it with 0xff + const int ch2 = get(); + if (ch2 == EOF) { + // a dangling last byte is dropped + inputEnd = true; + return EOF; + } + const int ch16 = makeUtf16Char(static_cast(ch), static_cast(ch2)); + return (ch16 >= 0x80) ? 0xff : ch16; + } + inline int makeUtf16Char(const unsigned char ch, const unsigned char ch2) const { return (bom == 0xfeff) ? (ch<<8 | ch2) : (ch2<<8 | ch); } @@ -338,156 +425,154 @@ class simplecpp::TokenList::Stream { } unsigned short bom; + + /** characters that are read but not used yet. The next character is at the back. */ + std::string buffer; + /** the last character returned by readChar(), EOF if ungetChar() has nothing to put back */ + int lastChar{EOF}; + /** readChar() has returned EOF */ + bool atEnd{false}; + /** get() has returned EOF */ + bool inputEnd{false}; protected: bool isUtf16; }; -class StdIStream : public simplecpp::TokenList::Stream { -public: - // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members - explicit StdIStream(std::istream &istr) - : istr(istr) { - assert(istr.good()); - init(); - } +namespace { + class StdIStream : public simplecpp::TokenList::Stream { + public: + // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members + explicit StdIStream(std::istream &istr) + : istr(istr) { + assert(istr.good()); + init(); + } - virtual int get() override { - return istr.get(); - } - virtual int peek() override { - return istr.peek(); - } - virtual void unget() override { - istr.unget(); - } - virtual bool good() override { - return istr.good(); - } + protected: + int get() override { + return istr.get(); + } + int peek() override { + return istr.peek(); + } + void unget() override { + istr.unget(); + } -private: - std::istream &istr; -}; + private: + std::istream &istr; + }; -class StdCharBufStream : public simplecpp::TokenList::Stream { -public: - // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members - StdCharBufStream(const unsigned char* str, std::size_t size) - : str(str) - , size(size) - , pos(0) - , lastStatus(0) { - init(); - } + class StdCharBufStream : public simplecpp::TokenList::Stream { + public: + // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members + StdCharBufStream(const unsigned char* str, std::size_t size) + : str(str) + , size(size) + { + init(); + } - virtual int get() override { - if (pos >= size) - return lastStatus = EOF; - return str[pos++]; - } - virtual int peek() override { - if (pos >= size) - return lastStatus = EOF; - return str[pos]; - } - virtual void unget() override { - --pos; - } - virtual bool good() override { - return lastStatus != EOF; - } + protected: + int get() override { + if (pos >= size) + return EOF; + return str[pos++]; + } + int peek() override { + if (pos >= size) + return EOF; + return str[pos]; + } + void unget() override { + --pos; + } -private: - const unsigned char *str; - const std::size_t size; - std::size_t pos; - int lastStatus; -}; + private: + const unsigned char *str; + const std::size_t size; + std::size_t pos{}; + }; -class FileStream : public simplecpp::TokenList::Stream { -public: - // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members - explicit FileStream(const std::string &filename, std::vector &files) - : file(fopen(filename.c_str(), "rb")) - , lastCh(0) - , lastStatus(0) { - if (!file) { - files.push_back(filename); - throw simplecpp::Output(files, simplecpp::Output::FILE_NOT_FOUND, "File is missing: " + filename); + class FileStream : public simplecpp::TokenList::Stream { + public: + /** + * @throws simplecpp::Output thrown if file is not found + */ + // cppcheck-suppress uninitDerivedMemberVar - we call Stream::init() to initialize the private members + explicit FileStream(const std::string &filename, std::vector &files) + : file(fopen(filename.c_str(), "rb")) + { + if (!file) { + files.emplace_back(filename); + throw simplecpp::Output(simplecpp::Output::FILE_NOT_FOUND, {}, "File is missing: " + filename); + } + init(); } - init(); - } - ~FileStream() override { - fclose(file); - file = nullptr; - } + FileStream(const FileStream&) = delete; + FileStream &operator=(const FileStream&) = delete; - virtual int get() override { - lastStatus = lastCh = fgetc(file); - return lastCh; - } - virtual int peek() override { - // keep lastCh intact - const int ch = fgetc(file); - unget_internal(ch); - return ch; - } - virtual void unget() override { - unget_internal(lastCh); - } - virtual bool good() override { - return lastStatus != EOF; - } + ~FileStream() override { + fclose(file); + file = nullptr; + } -private: - void unget_internal(int ch) { - if (isUtf16) { - // TODO: use ungetc() as well - // UTF-16 has subsequent unget() calls - fseek(file, -1, SEEK_CUR); - } else - ungetc(ch, file); - } + protected: + int get() override { + lastCh = fgetc(file); + return lastCh; + } + int peek() override { + // keep lastCh intact + const int ch = fgetc(file); + unget_internal(ch); + return ch; + } + void unget() override { + unget_internal(lastCh); + } - FileStream(const FileStream&); - FileStream &operator=(const FileStream&); + private: + void unget_internal(int ch) { + if (isUtf16) { + // TODO: use ungetc() as well + // UTF-16 has subsequent unget() calls + fseek(file, -1, SEEK_CUR); + } else { + ungetc(ch, file); + } + } - FILE *file; - int lastCh; - int lastStatus; -}; + FILE *file; + int lastCh{}; + }; +} simplecpp::TokenList::TokenList(std::vector &filenames) : frontToken(nullptr), backToken(nullptr), files(filenames) {} -simplecpp::TokenList::TokenList(std::istream &istr, std::vector &filenames, const std::string &filename, OutputList *outputList) +simplecpp::TokenList::TokenList(std::istream &istr, std::vector &filenames, const std::string &filename, const DUI &dui, OutputList *outputList) : frontToken(nullptr), backToken(nullptr), files(filenames) { StdIStream stream(istr); - readfile(stream,filename,outputList); + readfile(stream,filename,dui,outputList); } -simplecpp::TokenList::TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename, OutputList *outputList) +simplecpp::TokenList::TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename, const DUI &dui, OutputList *outputList, int /*unused*/) : frontToken(nullptr), backToken(nullptr), files(filenames) { StdCharBufStream stream(data, size); - readfile(stream,filename,outputList); -} - -simplecpp::TokenList::TokenList(const char* data, std::size_t size, std::vector &filenames, const std::string &filename, OutputList *outputList) - : frontToken(nullptr), backToken(nullptr), files(filenames) -{ - StdCharBufStream stream(reinterpret_cast(data), size); - readfile(stream,filename,outputList); + readfile(stream,filename,dui,outputList); } -simplecpp::TokenList::TokenList(const std::string &filename, std::vector &filenames, OutputList *outputList) +simplecpp::TokenList::TokenList(const std::string &filename, std::vector &filenames, const DUI &dui, OutputList *outputList) : frontToken(nullptr), backToken(nullptr), files(filenames) { try { FileStream stream(filename, filenames); - readfile(stream,filename,outputList); - } catch (const simplecpp::Output & e) { // TODO handle extra type of errors - outputList->push_back(e); + readfile(stream,filename,dui,outputList); + } catch (const simplecpp::Output & e) { + outputList->emplace_back(e); } } @@ -553,24 +638,34 @@ void simplecpp::TokenList::push_back(Token *tok) backToken = tok; } -void simplecpp::TokenList::dump() const +void simplecpp::TokenList::dump(bool linenrs) const { - std::cout << stringify() << std::endl; + std::cout << stringify(linenrs) << std::endl; } -std::string simplecpp::TokenList::stringify() const +std::string simplecpp::TokenList::stringify(bool linenrs) const { std::ostringstream ret; - Location loc(files); + Location loc; + loc.line = 1; + bool filechg = true; for (const Token *tok = cfront(); tok; tok = tok->next) { if (tok->location.line < loc.line || tok->location.fileIndex != loc.fileIndex) { - ret << "\n#line " << tok->location.line << " \"" << tok->location.file() << "\"\n"; + ret << "\n#line " << tok->location.line << " \"" << file(tok->location) << "\"\n"; loc = tok->location; + filechg = true; + } + + if (linenrs && filechg) { + ret << loc.line << ": "; + filechg = false; } while (tok->location.line > loc.line) { ret << '\n'; loc.line++; + if (linenrs) + ret << loc.line << ": "; } if (sameline(tok->previous, tok)) @@ -584,7 +679,7 @@ std::string simplecpp::TokenList::stringify() const return ret.str(); } -static bool isNameChar(unsigned char ch) +static bool isNameChar(int ch) { return std::isalnum(ch) || ch == '_' || ch == '$'; } @@ -603,15 +698,16 @@ static std::string escapeString(const std::string &str) return ostr.str(); } -static void portabilityBackslash(simplecpp::OutputList *outputList, const std::vector &files, const simplecpp::Location &location) +static void portabilityBackslash(simplecpp::OutputList *outputList, const simplecpp::Location &location) { if (!outputList) return; - simplecpp::Output err(files); - err.type = simplecpp::Output::PORTABILITY_BACKSLASH; - err.location = location; - err.msg = "Combination 'backslash space newline' is not portable."; - outputList->push_back(err); + simplecpp::Output err{ + simplecpp::Output::PORTABILITY_BACKSLASH, + location, + "Combination 'backslash space newline' is not portable." + }; + outputList->emplace_back(std::move(err)); } static bool isStringLiteralPrefix(const std::string &str) @@ -620,16 +716,68 @@ static bool isStringLiteralPrefix(const std::string &str) str == "R" || str == "uR" || str == "UR" || str == "LR" || str == "u8R"; } -void simplecpp::TokenList::lineDirective(unsigned int fileIndex, unsigned int line, Location *location) +/** Spelling of a token in the result of the # operator: a \ is inserted + * before each " and \ of a string literal or character constant (C 6.10.3.2p2) */ +static std::string stringifyToken(const std::string &str) +{ + const std::string::size_type quote = str.find_first_of("\"\'"); + if (quote == std::string::npos || str.back() != str[quote] || + (quote > 0 && !isStringLiteralPrefix(str.substr(0, quote)))) + return str; + std::string ret; + for (const char c : str) { + if (c == '\\' || c == '\"') + ret += '\\'; + ret += c; + } + return ret; +} + +/** + * The result of the # operator: the tokens as a string literal (C 6.10.3.2). + * The tokens are separated by a space where there was whitespace between them. + * @param tokens the argument of #, not macro expanded + * @param loc location for the warning + * @param macroName the macro that is expanded, for the warning + * @param outputList if not null, a warning is added when a final backslash is dropped + * @return the string literal, including the quotes + */ +static std::string stringify(const simplecpp::TokenList &tokens, const simplecpp::Location &loc, const std::string ¯oName, simplecpp::OutputList *outputList) { - if (fileIndex != location->fileIndex || line >= location->line) { - location->fileIndex = fileIndex; - location->line = line; + std::string str; + for (const simplecpp::Token *tok = tokens.cfront(), *next; tok; tok = next) { + next = tok->next; + str += stringifyToken(tok->str()); + if (next && tok->whitespaceahead) + str += ' '; + } + // An odd number of trailing backslashes would escape the closing quote and + // yield an unterminated string literal (undefined behavior, C 6.10.3.2p2). + // Drop the final backslash, as gcc and clang do. + std::size_t trailing = 0; + while (trailing < str.size() && str[str.size() - 1U - trailing] == '\\') + ++trailing; + if (trailing % 2U == 1U) { + str.pop_back(); + if (outputList) { + outputList->emplace_back(simplecpp::Output::PORTABILITY_STRINGIFY, loc, + "Invalid string literal when expanding \'" + macroName + "\', ignoring final \'\\\'. " + "Undefined behavior according to C standard chapter 6.10.3.2, paragraph 2."); + } + } + return '\"' + str + '\"'; +} + +void simplecpp::TokenList::lineDirective(unsigned int fileIndex_, unsigned int line, Location &location) +{ + if (fileIndex_ != location.fileIndex || line >= location.line) { + location.fileIndex = fileIndex_; + location.line = line; return; } - if (line + 2 >= location->line) { - location->line = line; + if (line + 2 >= location.line) { + location.line = line; while (cback()->op != '#') deleteToken(back()); deleteToken(back()); @@ -639,32 +787,159 @@ void simplecpp::TokenList::lineDirective(unsigned int fileIndex, unsigned int li static const std::string COMMENT_END("*/"); -void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, OutputList *outputList) +/** + * Read a name or a number. + * In a number, a ' digit separator is removed, so 1'000 gives "1000". + * The stream is left after the last character of the token. + * @param stream the stream to read from + * @param ch the first character, already read from the stream + * @return the name or number + */ +static simplecpp::TokenString readNameOrNumber(simplecpp::TokenList::Stream &stream, unsigned char ch) +{ + simplecpp::TokenString currentToken; + const bool num = !!std::isdigit(ch); + while (stream.good() && isNameChar(ch)) { + currentToken += ch; + ch = stream.readChar(); + if (num && ch=='\'' && isNameChar(stream.peekChar())) + ch = stream.readChar(); + } + stream.ungetChar(); + return currentToken; +} + +/** + * Read a // comment, up to but not including the newline. + * A backslash followed by a newline continues the comment on the next line. + * The backslash, and any spaces or tabs after it, are then replaced by a + * newline in the comment text. + * @param stream the stream to read from + * @param ch the first '/', already read from the stream + * @param location the location of the comment, used for warnings + * @param multiline incremented for each continued line + * @param outputList if not null, a portability warning is added when there is + * whitespace between the backslash and the newline + * @return the comment text, including the leading // + */ +static simplecpp::TokenString readLineComment(simplecpp::TokenList::Stream &stream, unsigned char ch, const simplecpp::Location &location, unsigned int &multiline, simplecpp::OutputList *outputList) { - std::stack loc; + simplecpp::TokenString currentToken; + while (stream.good() && ch != '\n') { + currentToken += ch; + ch = stream.readChar(); + if (ch == '\\') { + simplecpp::TokenString tmp; + char tmp_ch = ch; + while ((stream.good()) && (tmp_ch == '\\' || tmp_ch == ' ' || tmp_ch == '\t')) { + tmp += tmp_ch; + tmp_ch = stream.readChar(); + } + if (!stream.good()) { + break; + } + if (tmp_ch != '\n') { + currentToken += tmp; + } else { + const simplecpp::TokenString check_portability = currentToken + tmp; + const std::string::size_type pos = check_portability.find_last_not_of(" \t"); + if (pos < check_portability.size() - 1U && check_portability[pos] == '\\') + portabilityBackslash(outputList, location); + ++multiline; + tmp_ch = stream.readChar(); + currentToken += '\n'; + } + ch = tmp_ch; + } + } + if (ch == '\n') { + stream.ungetChar(); + } + return currentToken; +} + +simplecpp::TokenString simplecpp::TokenList::readBlockComment(Stream &stream, unsigned int &multiline) +{ + TokenString currentToken = "/*"; + (void)stream.readChar(); + unsigned char ch = stream.readChar(); + while (stream.good()) { + currentToken += ch; + if (currentToken.size() >= 4U && endsWith(currentToken, COMMENT_END)) + break; + ch = stream.readChar(); + } + // multiline.. + + std::string::size_type pos = 0; + while ((pos = currentToken.find("\\\n",pos)) != std::string::npos) { + currentToken.erase(pos,2); + ++multiline; + } + if (multiline || isLastLinePreprocessor()) { + pos = 0; + while ((pos = currentToken.find('\n',pos)) != std::string::npos) { + currentToken.erase(pos,1); + ++multiline; + } + } + return currentToken; +} + +void simplecpp::TokenList::readErrorText(Stream &stream, unsigned char ch, Location &location) +{ + TokenString currentToken; + char prev = ' '; + while (stream.good() && (prev == '\\' || (ch != '\r' && ch != '\n'))) { + currentToken += ch; + prev = ch; + ch = stream.readChar(); + } + stream.ungetChar(); + std::string::size_type pos = 0; + unsigned int spliced = 0; + while ((pos = currentToken.find('\\', pos)) != std::string::npos) { + if (pos + 1 < currentToken.size() && currentToken[pos + 1] == '\n') { + currentToken.erase(pos, 2); + ++spliced; + } else { + ++pos; + } + } + if (!currentToken.empty()) { + push_back(new Token(currentToken, location)); + location.adjust(currentToken); + } + location.line += spliced; +} + +void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, const DUI &dui, OutputList *outputList) +{ unsigned int multiline = 0U; + bool trailing_nl = true; const Token *oldLastToken = nullptr; - Location location(files); - location.fileIndex = fileIndex(filename); - location.line = 1U; - location.col = 1U; + const cstd_t cstd = getCStd(dui.std); + const cppstd_t cppstd = getCppStd(dui.std); + + Location location(fileIndex(filename), 1, 1); while (stream.good()) { unsigned char ch = stream.readChar(); if (!stream.good()) break; + trailing_nl = false; + if (ch >= 0x80) { if (outputList) { - simplecpp::Output err(files); - err.type = simplecpp::Output::UNHANDLED_CHAR_ERROR; - err.location = location; - std::ostringstream s; - s << static_cast(ch); - err.msg = "The code contains unhandled character(s) (character code=" + s.str() + "). Neither unicode nor extended ascii is supported."; - outputList->push_back(err); + simplecpp::Output err{ + simplecpp::Output::UNHANDLED_CHAR_ERROR, + location, + "The code contains unhandled character(s) (character code=" + std::to_string(static_cast(ch)) + "). Neither unicode nor extended ascii is supported." + }; + outputList->emplace_back(std::move(err)); } clear(); return; @@ -673,48 +948,111 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, if (ch == '\n') { if (cback() && cback()->op == '\\') { if (location.col > cback()->location.col + 1U) - portabilityBackslash(outputList, files, cback()->location); + portabilityBackslash(outputList, cback()->location); ++multiline; deleteToken(back()); } else { location.line += multiline + 1; multiline = 0U; + trailing_nl = true; } if (!multiline) location.col = 1; if (oldLastToken != cback()) { oldLastToken = cback(); - if (!isLastLinePreprocessor()) + + // #line 3 + // #line 3 "file.c" + // #3 + // #3 "file.c" + const Token * ppTok = isLastLinePreprocessor(); + if (!ppTok) + continue; + + const auto advanceAndSkipComments = [](const Token* tok) { + do { + tok = tok->next; + } while (tok && tok->comment); + return tok; + }; + + // skip # + ppTok = advanceAndSkipComments(ppTok); + if (!ppTok) + continue; + + if (ppTok->str() == "line") + ppTok = advanceAndSkipComments(ppTok); + + if (ppTok && (ppTok->str()[0] == '-' || ppTok->str()[0] == '+')) { + if (outputList) { + simplecpp::Output err{ + simplecpp::Output::SYNTAX_ERROR, + location, + "Invalid character in line directive: '" + ppTok->str() + "'." + }; + outputList->emplace_back(std::move(err)); + } + clear(); + return; + } + + if (!ppTok || !ppTok->number) continue; - const std::string lastline(lastLine()); - if (lastline == "# file %str%") { - const Token *strtok = cback(); - while (strtok->comment) - strtok = strtok->previous; - loc.push(location); - location.fileIndex = fileIndex(strtok->str().substr(1U, strtok->str().size() - 2U)); - location.line = 1U; - } else if (lastline == "# line %num%") { - const Token *numtok = cback(); - while (numtok->comment) - numtok = numtok->previous; - lineDirective(location.fileIndex, std::atol(numtok->str().c_str()), &location); - } else if (lastline == "# %num% %str%" || lastline == "# line %num% %str%") { - const Token *strtok = cback(); - while (strtok->comment) - strtok = strtok->previous; - const Token *numtok = strtok->previous; - while (numtok->comment) - numtok = numtok->previous; - lineDirective(fileIndex(replaceAll(strtok->str().substr(1U, strtok->str().size() - 2U),"\\\\","\\")), - std::atol(numtok->str().c_str()), &location); - } - // #endfile - else if (lastline == "# endfile" && !loc.empty()) { - location = loc.top(); - loc.pop(); + + constexpr unsigned long line_limit = std::numeric_limits::max(); + unsigned long line; + try { + line = std::min(line_limit, std::stoul(ppTok->str())); + } catch (...) { + line = line_limit; + } + + unsigned long maxline; + if ((cstd != CUnknown && cstd < C99) || (cppstd != CPPUnknown && cppstd < CPP11)) + maxline = 32767; + else + maxline = 2147483647; + + if (line == 0 || line > maxline) { + if (outputList) { + const bool unknown_std = cstd == CUnknown && cppstd == CPPUnknown; + std::string msg = "Line number out of range: " + ppTok->str() + ". "; + if (line == 0) { + msg += "Line number zero is undefined behavior."; + } else { + msg += "Line numbers above " + std::to_string(maxline) + " are "; + if (unknown_std) + msg += "undefined behavior or conditionally supported"; + else if (cppstd >= CPP26) + msg += "conditionally supported"; + else + msg += "undefined behavior"; + if (cstd != CUnknown) + msg += std::string(" in ") + getCStdName(cstd); + else if (cppstd != CPPUnknown) + msg += std::string(" in ") + getCppStdName(cppstd); + msg += "."; + } + simplecpp::Output err{ + simplecpp::Output::PORTABILITY_LINE_DIRECTIVE, + location, msg + }; + outputList->emplace_back(std::move(err)); + } } + + ppTok = advanceAndSkipComments(ppTok); + + unsigned int fileindex; + + if (ppTok && ppTok->str()[0] == '\"') + fileindex = fileIndex(replaceAll(ppTok->str().substr(1U, ppTok->str().size() - 2U),"\\\\","\\")); + else + fileindex = location.fileIndex; + + lineDirective(fileindex, line, location); } continue; @@ -728,94 +1066,26 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, TokenString currentToken; if (cback() && cback()->location.line == location.line && cback()->previous && cback()->previous->op == '#') { - const Token* const llTok = lastLineTok(); - if (llTok && llTok->op == '#' && llTok->next && (llTok->next->str() == "error" || llTok->next->str() == "warning")) { - char prev = ' '; - while (stream.good() && (prev == '\\' || (ch != '\r' && ch != '\n'))) { - currentToken += ch; - prev = ch; - ch = stream.readChar(); - } - stream.ungetChar(); - push_back(new Token(currentToken, location)); - location.adjust(currentToken); + const Token* const ppTok = cback()->previous; + if (ppTok->next && (ppTok->next->str() == "error" || ppTok->next->str() == "warning")) { + readErrorText(stream, ch, location); continue; } } // number or name if (isNameChar(ch)) { - const bool num = !!std::isdigit(ch); - while (stream.good() && isNameChar(ch)) { - currentToken += ch; - ch = stream.readChar(); - if (num && ch=='\'' && isNameChar(stream.peekChar())) - ch = stream.readChar(); - } - - stream.ungetChar(); + currentToken = readNameOrNumber(stream, ch); } // comment else if (ch == '/' && stream.peekChar() == '/') { - while (stream.good() && ch != '\n') { - currentToken += ch; - ch = stream.readChar(); - if(ch == '\\') { - TokenString tmp; - char tmp_ch = ch; - while((stream.good()) && (tmp_ch == '\\' || tmp_ch == ' ' || tmp_ch == '\t')) { - tmp += tmp_ch; - tmp_ch = stream.readChar(); - } - if(!stream.good()) { - break; - } - - if(tmp_ch != '\n') { - currentToken += tmp; - } else { - const TokenString check_portability = currentToken + tmp; - const std::string::size_type pos = check_portability.find_last_not_of(" \t"); - if (pos < check_portability.size() - 1U && check_portability[pos] == '\\') - portabilityBackslash(outputList, files, location); - ++multiline; - tmp_ch = stream.readChar(); - currentToken += '\n'; - } - ch = tmp_ch; - } - } - if (ch == '\n') { - stream.ungetChar(); - } + currentToken = readLineComment(stream, ch, location, multiline, outputList); } // comment else if (ch == '/' && stream.peekChar() == '*') { - currentToken = "/*"; - (void)stream.readChar(); - ch = stream.readChar(); - while (stream.good()) { - currentToken += ch; - if (currentToken.size() >= 4U && endsWith(currentToken, COMMENT_END)) - break; - ch = stream.readChar(); - } - // multiline.. - - std::string::size_type pos = 0; - while ((pos = currentToken.find("\\\n",pos)) != std::string::npos) { - currentToken.erase(pos,2); - ++multiline; - } - if (multiline || isLastLinePreprocessor()) { - pos = 0; - while ((pos = currentToken.find('\n',pos)) != std::string::npos) { - currentToken.erase(pos,1); - ++multiline; - } - } + currentToken = readBlockComment(stream, multiline); } // string / char literal @@ -838,24 +1108,26 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, } if (!stream.good() || ch == '\n') { if (outputList) { - Output err(files); - err.type = Output::SYNTAX_ERROR; - err.location = location; - err.msg = "Invalid newline in raw string delimiter."; - outputList->push_back(err); + Output err{ + Output::SYNTAX_ERROR, + location, + "Invalid newline in raw string delimiter." + }; + outputList->emplace_back(std::move(err)); } return; } const std::string endOfRawString(')' + delim + currentToken); - while (stream.good() && !(endsWith(currentToken, endOfRawString) && currentToken.size() > 1)) + while (stream.good() && (!endsWith(currentToken, endOfRawString) || currentToken.size() <= 1)) currentToken += stream.readChar(); if (!endsWith(currentToken, endOfRawString)) { if (outputList) { - Output err(files); - err.type = Output::SYNTAX_ERROR; - err.location = location; - err.msg = "Raw string missing terminating delimiter."; - outputList->push_back(err); + Output err{ + Output::SYNTAX_ERROR, + location, + "Raw string missing terminating delimiter." + }; + outputList->emplace_back(std::move(err)); } return; } @@ -865,7 +1137,7 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, back()->setstr(currentToken); location.adjust(currentToken); if (currentToken.find_first_of("\r\n") == std::string::npos) - location.col += 2 + 2 * delim.size(); + location.col += 2 + (2 * delim.size()); else location.col += 1 + delim.size(); @@ -924,9 +1196,35 @@ void simplecpp::TokenList::readfile(Stream &stream, const std::string &filename, location.adjust(currentToken); } + if ((cstd != CUnknown || cppstd == CPPUnknown) && !trailing_nl && outputList) { + Output err{ + Output::PORTABILITY_NO_EOF_NEWLINE, + location, + "No newline at end of file is undefined behavior in C." + }; + outputList->emplace_back(std::move(err)); + } + combineOperators(); } +/** + * If one operand of the binary operator tok is an error value then + * "lhs op rhs" is replaced by that error value. + * @return the error value token, or nullptr + */ +static simplecpp::Token *foldErrorValue(simplecpp::TokenList &tokens, simplecpp::Token *tok) +{ + simplecpp::Token * const lhs = tok->previous; + if (!isErrorValue(lhs) && !isErrorValue(tok->next)) + return nullptr; + if (!isErrorValue(lhs)) + lhs->setstr(tok->next->str()); + tokens.deleteToken(tok->next); + tokens.deleteToken(tok); + return lhs; +} + void simplecpp::TokenList::constFold() { while (cfront()) { @@ -947,7 +1245,7 @@ void simplecpp::TokenList::constFold() constFoldComparison(tok); constFoldBitwise(tok); constFoldLogicalOp(tok); - constFoldQuestionOp(&tok); + constFoldQuestionOp(tok); // If there is no '(' we are done with the constant folding if (tok->op != '(') @@ -960,6 +1258,11 @@ void simplecpp::TokenList::constFold() deleteToken(tok->previous); deleteToken(tok->next); } + + for (const Token *tok = cfront(); tok; tok = tok->next) { + if (isErrorValue(tok)) + throw std::overflow_error(tok->str().substr(ERROR_VALUE.size())); + } } static bool isFloatSuffix(const simplecpp::Token *tok) @@ -970,34 +1273,100 @@ static bool isFloatSuffix(const simplecpp::Token *tok) return c == 'f' || c == 'l'; } +static const std::string AND("and"); +static const std::string BITAND("bitand"); +static const std::string BITOR("bitor"); +static bool isAlternativeAndBitandBitor(const simplecpp::Token* tok) +{ + return isAlternativeBinaryOp(tok, AND) || isAlternativeBinaryOp(tok, BITAND) || isAlternativeBinaryOp(tok, BITOR); +} + +/** + * Keep track of whether the code is in a function body. This is not part of + * preprocessing, it is used by the cppcheck-specific handling of &= in + * combineOperators(). + * @param executableScope one entry for each open {, true if it is a function + * body or a block in a function body + * @param tok the current token + * @return true if tok is { or } + */ +static bool updateExecutableScope(std::stack> &executableScope, const simplecpp::Token *tok) +{ + if (tok->op == '{') { + if (executableScope.top()) { + executableScope.push(true); + return true; + } + const simplecpp::Token *prev = tok->previous; + while (prev && prev->isOneOf(";{}(")) + prev = prev->previous; + executableScope.push(prev && prev->op == ')'); + return true; + } + if (tok->op == '}') { + if (executableScope.size() > 1) + executableScope.pop(); + return true; + } + return false; +} + +/** + * Is tok the & of an unnamed reference parameter with a default value in a + * function declaration, as in "void f(x&=2)"? Then "&" and "=" should not be + * combined into "&=". This is a cppcheck-specific heuristic, compilers + * tokenize "&=" as one operator. + * @param tok a & token that is directly followed by = + */ +static bool isAnonymousReferenceParameter(const simplecpp::Token *tok) +{ + int indentlevel = 0; + const simplecpp::Token *start = tok; + while (indentlevel >= 0 && start) { + if (start->op == ')') + ++indentlevel; + else if (start->op == '(') + --indentlevel; + else if (start->isOneOf(";{}")) + break; + start = start->previous; + } + if (indentlevel == -1 && start) { + const simplecpp::Token * const ftok = start; + bool isFuncDecl = ftok->name; + while (isFuncDecl) { + if (!start->name && start->str() != "::" && start->op != '*' && start->op != '&') + isFuncDecl = false; + if (!start->previous) + break; + if (start->previous->isOneOf(";{}:")) + break; + start = start->previous; + } + isFuncDecl &= start != ftok && start->name; + if (isFuncDecl) { + // TODO: we could loop through the parameters here and check if they are correct. + return true; + } + } + return false; +} + void simplecpp::TokenList::combineOperators() { - std::stack executableScope; - executableScope.push(false); + std::stack> executableScope{{false}}; for (Token *tok = front(); tok; tok = tok->next) { - if (tok->op == '{') { - if (executableScope.top()) { - executableScope.push(true); - continue; - } - const Token *prev = tok->previous; - while (prev && prev->isOneOf(";{}()")) - prev = prev->previous; - executableScope.push(prev && prev->op == ')'); - continue; - } - if (tok->op == '}') { - if (executableScope.size() > 1) - executableScope.pop(); + if (updateExecutableScope(executableScope, tok)) continue; - } if (tok->op == '.') { // ellipsis ... if (tok->next && tok->next->op == '.' && tok->next->location.col == (tok->location.col + 1) && tok->next->next && tok->next->next->op == '.' && tok->next->next->location.col == (tok->location.col + 2)) { tok->setstr("..."); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); continue; } @@ -1005,13 +1374,15 @@ void simplecpp::TokenList::combineOperators() if (tok->previous && tok->previous->number && sameline(tok->previous, tok) && tok->previous->str().find_first_of("._") == std::string::npos) { tok->setstr(tok->previous->str() + '.'); deleteToken(tok->previous); - if (sameline(tok, tok->next) && (isFloatSuffix(tok->next) || (tok->next && tok->next->startsWithOneOf("AaBbCcDdEeFfPp")))) { + if (sameline(tok, tok->next) && (isFloatSuffix(tok->next) || (tok->next && tok->next->startsWithOneOf("AaBbCcDdEeFfPp") && !isAlternativeAndBitandBitor(tok->next)))) { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } } if (tok->next && tok->next->number) { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } } @@ -1022,7 +1393,9 @@ void simplecpp::TokenList::combineOperators() (isHex(tok->str()) && (lastChar == 'P' || lastChar == 'p'))) && tok->next && tok->next->isOneOf("+-") && tok->next->next && tok->next->next->number) { tok->setstr(tok->str() + tok->next->op + tok->next->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } @@ -1034,55 +1407,32 @@ void simplecpp::TokenList::combineOperators() continue; if (tok->next->op == '=' && tok->isOneOf("=!<>+-*/%&|^")) { - if (tok->op == '&' && !executableScope.top()) { - // don't combine &= if it is a anonymous reference parameter with default value: - // void f(x&=2) - int indentlevel = 0; - const Token *start = tok; - while (indentlevel >= 0 && start) { - if (start->op == ')') - ++indentlevel; - else if (start->op == '(') - --indentlevel; - else if (start->isOneOf(";{}")) - break; - start = start->previous; - } - if (indentlevel == -1 && start) { - const Token * const ftok = start; - bool isFuncDecl = ftok->name; - while (isFuncDecl) { - if (!start->name && start->str() != "::" && start->op != '*' && start->op != '&') - isFuncDecl = false; - if (!start->previous) - break; - if (start->previous->isOneOf(";{}:")) - break; - start = start->previous; - } - isFuncDecl &= start != ftok && start->name; - if (isFuncDecl) { - // TODO: we could loop through the parameters here and check if they are correct. - continue; - } - } - } + // don't combine &= if it is a anonymous reference parameter with default value: + // void f(x&=2) + if (tok->op == '&' && !executableScope.top() && isAnonymousReferenceParameter(tok)) + continue; tok->setstr(tok->str() + "="); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } else if ((tok->op == '|' || tok->op == '&') && tok->op == tok->next->op) { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } else if (tok->op == ':' && tok->next->op == ':') { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } else if (tok->op == '-' && tok->next->op == '>') { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } else if ((tok->op == '<' || tok->op == '>') && tok->op == tok->next->op) { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); if (tok->next && tok->next->op == '=' && tok->next->next && tok->next->next->op != '=') { tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } } else if ((tok->op == '+' || tok->op == '-') && tok->op == tok->next->op) { @@ -1093,6 +1443,7 @@ void simplecpp::TokenList::combineOperators() if (tok->next->next && tok->next->next->number) continue; tok->setstr(tok->str() + tok->next->str()); + tok->whitespaceahead = tok->next->whitespaceahead; deleteToken(tok->next); } } @@ -1100,9 +1451,16 @@ void simplecpp::TokenList::combineOperators() static const std::string COMPL("compl"); static const std::string NOT("not"); -void simplecpp::TokenList::constFoldUnaryNotPosNeg(simplecpp::Token *tok) +void simplecpp::TokenList::constFoldUnaryNotPosNeg(simplecpp::Token *tok1) { - for (; tok && tok->op != ')'; tok = tok->next) { + if (!tok1 || tok1->op == ')') + return; + Token *last = tok1; + while (last->next && last->next->op != ')') + last = last->next; + + // unary operators are right-associative, "!-0" means "!(-0)", so they are folded from right to left + for (Token *tok = last; tok; tok = (tok == tok1) ? nullptr : tok->previous) { // "not" might be ! if (isAlternativeUnaryOp(tok, NOT)) tok->op = '!'; @@ -1110,24 +1468,37 @@ void simplecpp::TokenList::constFoldUnaryNotPosNeg(simplecpp::Token *tok) else if (isAlternativeUnaryOp(tok, COMPL)) tok->op = '~'; - if (tok->op == '!' && tok->next && tok->next->number) { - tok->setstr(tok->next->str() == "0" ? "1" : "0"); + if (tok->isOneOf("!~") && isErrorValue(tok->next)) { + tok->setstr(tok->next->str()); + deleteToken(tok->next); + } else if (tok->op == '!' && tok->next && tok->next->number) { + tok->setstr(stringToLL(tok->next->str()) == 0 ? "1" : "0"); deleteToken(tok->next); } else if (tok->op == '~' && tok->next && tok->next->number) { tok->setstr(toString(~stringToLL(tok->next->str()))); deleteToken(tok->next); } else { - if (tok->previous && (tok->previous->number || tok->previous->name)) + if (tok->previous && (isValue(tok->previous) || (tok->previous->name && !isAlternativeOpName(tok->previous)))) + continue; + if (!isValue(tok->next)) continue; - if (!tok->next || !tok->next->number) + if (isErrorValue(tok->next)) { + if (tok->isOneOf("+-")) { + tok->setstr(tok->next->str()); + deleteToken(tok->next); + } continue; + } switch (tok->op) { case '+': tok->setstr(tok->next->str()); deleteToken(tok->next); break; case '-': - tok->setstr(tok->op + tok->next->str()); + if (tok->next->str()[0] == '-') + tok->setstr(tok->next->str().substr(1)); + else + tok->setstr(tok->op + tok->next->str()); deleteToken(tok->next); break; } @@ -1138,30 +1509,33 @@ void simplecpp::TokenList::constFoldUnaryNotPosNeg(simplecpp::Token *tok) void simplecpp::TokenList::constFoldMulDivRem(Token *tok) { for (; tok && tok->op != ')'; tok = tok->next) { - if (!tok->previous || !tok->previous->number) + if (!isValue(tok->previous) || !isValue(tok->next)) continue; - if (!tok->next || !tok->next->number) + if (!tok->isOneOf("*/%")) continue; + if (Token * const errorValue = foldErrorValue(*this, tok)) { + tok = errorValue; + continue; + } - long long result; - if (tok->op == '*') - result = (stringToLL(tok->previous->str()) * stringToLL(tok->next->str())); - else if (tok->op == '/' || tok->op == '%') { + std::string result; + if (tok->op == '*') { + result = toString(stringToLL(tok->previous->str()) * stringToLL(tok->next->str())); + } else { const long long rhs = stringToLL(tok->next->str()); - if (rhs == 0) - throw std::overflow_error("division/modulo by zero"); const long long lhs = stringToLL(tok->previous->str()); - if (rhs == -1 && lhs == std::numeric_limits::min()) - throw std::overflow_error("division overflow"); - if (tok->op == '/') - result = (lhs / rhs); + if (rhs == 0) + result = ERROR_VALUE + "division/modulo by zero"; + else if (rhs == -1 && lhs == std::numeric_limits::min()) + result = ERROR_VALUE + "division overflow"; + else if (tok->op == '/') + result = toString(lhs / rhs); else - result = (lhs % rhs); - } else - continue; + result = toString(lhs % rhs); + } tok = tok->previous; - tok->setstr(toString(result)); + tok->setstr(result); deleteToken(tok->next); deleteToken(tok->next); } @@ -1170,10 +1544,14 @@ void simplecpp::TokenList::constFoldMulDivRem(Token *tok) void simplecpp::TokenList::constFoldAddSub(Token *tok) { for (; tok && tok->op != ')'; tok = tok->next) { - if (!tok->previous || !tok->previous->number) + if (!isValue(tok->previous) || !isValue(tok->next)) continue; - if (!tok->next || !tok->next->number) + if (!tok->isOneOf("+-")) continue; + if (Token * const errorValue = foldErrorValue(*this, tok)) { + tok = errorValue; + continue; + } long long result; if (tok->op == '+') @@ -1193,10 +1571,14 @@ void simplecpp::TokenList::constFoldAddSub(Token *tok) void simplecpp::TokenList::constFoldShift(Token *tok) { for (; tok && tok->op != ')'; tok = tok->next) { - if (!tok->previous || !tok->previous->number) + if (!isValue(tok->previous) || !isValue(tok->next)) + continue; + if (tok->str() != "<<" && tok->str() != ">>") continue; - if (!tok->next || !tok->next->number) + if (Token * const errorValue = foldErrorValue(*this, tok)) { + tok = errorValue; continue; + } long long result; if (tok->str() == "<<") @@ -1220,12 +1602,14 @@ void simplecpp::TokenList::constFoldComparison(Token *tok) if (isAlternativeBinaryOp(tok,NOTEQ)) tok->setstr("!="); - if (!tok->startsWithOneOf("<>=!")) + if (!tok->isOneOf("<>") && tok->str() != "<=" && tok->str() != ">=" && tok->str() != "==" && tok->str() != "!=") continue; - if (!tok->previous || !tok->previous->number) + if (!isValue(tok->previous) || !isValue(tok->next)) continue; - if (!tok->next || !tok->next->number) + if (Token * const errorValue = foldErrorValue(*this, tok)) { + tok = errorValue; continue; + } int result; if (tok->str() == "==") @@ -1250,8 +1634,6 @@ void simplecpp::TokenList::constFoldComparison(Token *tok) } } -static const std::string BITAND("bitand"); -static const std::string BITOR("bitor"); static const std::string XOR("xor"); void simplecpp::TokenList::constFoldBitwise(Token *tok) { @@ -1267,10 +1649,12 @@ void simplecpp::TokenList::constFoldBitwise(Token *tok) for (tok = tok1; tok && tok->op != ')'; tok = tok->next) { if (tok->op != *op && !isAlternativeBinaryOp(tok, *alternativeOp)) continue; - if (!tok->previous || !tok->previous->number) + if (!isValue(tok->previous) || !isValue(tok->next)) continue; - if (!tok->next || !tok->next->number) + if (Token * const errorValue = foldErrorValue(*this, tok)) { + tok = errorValue; continue; + } long long result; if (*op == '&') result = (stringToLL(tok->previous->str()) & stringToLL(tok->next->str())); @@ -1286,62 +1670,85 @@ void simplecpp::TokenList::constFoldBitwise(Token *tok) } } -static const std::string AND("and"); static const std::string OR("or"); void simplecpp::TokenList::constFoldLogicalOp(Token *tok) { - for (; tok && tok->op != ')'; tok = tok->next) { - if (tok->name) { - if (isAlternativeBinaryOp(tok,AND)) - tok->setstr("&&"); - else if (isAlternativeBinaryOp(tok,OR)) - tok->setstr("||"); + for (Token *t = tok; t && t->op != ')'; t = t->next) { + if (t->name) { + if (isAlternativeBinaryOp(t,AND)) + t->setstr("&&"); + else if (isAlternativeBinaryOp(t,OR)) + t->setstr("||"); } - if (tok->str() != "&&" && tok->str() != "||") - continue; - if (!tok->previous || !tok->previous->number) - continue; - if (!tok->next || !tok->next->number) - continue; + } - int result; - if (tok->str() == "||") - result = (stringToLL(tok->previous->str()) || stringToLL(tok->next->str())); - else /*if (tok->str() == "&&")*/ - result = (stringToLL(tok->previous->str()) && stringToLL(tok->next->str())); + // && has higher precedence than || + for (const char * const op : {"&&", "||"}) { + for (Token *t = tok; t && t->op != ')'; t = t->next) { + if (t->str() != op) + continue; + if (!isValue(t->previous) || !isValue(t->next)) + continue; - tok = tok->previous; - tok->setstr(toString(result)); - deleteToken(tok->next); - deleteToken(tok->next); + // the right operand is not evaluated if the left operand decides the result + std::string result; + if (isErrorValue(t->previous)) + result = t->previous->str(); + else if (t->str() == "||" && stringToLL(t->previous->str()) != 0) + result = "1"; + else if (t->str() == "&&" && stringToLL(t->previous->str()) == 0) + result = "0"; + else if (isErrorValue(t->next)) + result = t->next->str(); + else + result = (stringToLL(t->next->str()) != 0) ? "1" : "0"; + + t = t->previous; + t->setstr(result); + deleteToken(t->next); + deleteToken(t->next); + } } } -void simplecpp::TokenList::constFoldQuestionOp(Token **tok1) +void simplecpp::TokenList::constFoldQuestionOp(Token *&tok1) { - bool gotoTok1 = false; - for (Token *tok = *tok1; tok && tok->op != ')'; tok = gotoTok1 ? *tok1 : tok->next) { - gotoTok1 = false; - if (tok->str() != "?") - continue; + // ?: is right-associative, "a ? b : c ? d : e" means "a ? b : (c ? d : e)", + // so the last ? is folded first + std::vector questionTokens; + for (const Token *t = tok1; t && t->op != ')'; t = t->next) { + if (t->str() == "?") + questionTokens.push_back(t); + } + while (!questionTokens.empty()) { + const Token * const tok = questionTokens.back(); + questionTokens.pop_back(); if (!tok->previous || !tok->next || !tok->next->next) throw std::runtime_error("invalid expression"); - if (!tok->previous->number) - continue; - if (tok->next->next->op != ':') - continue; Token * const condTok = tok->previous; Token * const trueTok = tok->next; Token * const falseTok = trueTok->next->next; if (!falseTok) throw std::runtime_error("invalid expression"); - if (condTok == *tok1) - *tok1 = (condTok->str() != "0" ? trueTok : falseTok); - deleteToken(condTok->next); // ? - deleteToken(trueTok->next); // : - deleteToken(condTok->str() == "0" ? trueTok : falseTok); - deleteToken(condTok); - gotoTok1 = true; + if (trueTok->next->op != ':') + return; + if (!isValue(condTok) || !isValue(trueTok) || !isValue(falseTok)) + return; + if (isErrorValue(condTok)) { + // "error ? x : y" => error + deleteToken(condTok->next); // ? + deleteToken(trueTok->next); // : + deleteToken(trueTok); + deleteToken(falseTok); + } else { + Token * const result = (stringToLL(condTok->str()) != 0 ? trueTok : falseTok); + if (condTok == tok1) + tok1 = result; + deleteToken(condTok->next); // ? + deleteToken(trueTok->next); // : + deleteToken(result == falseTok ? trueTok : falseTok); // the operand that is not evaluated + deleteToken(condTok); + } } } @@ -1351,8 +1758,12 @@ void simplecpp::TokenList::removeComments() while (tok) { Token * const tok1 = tok; tok = tok->next; - if (tok1->comment) + if (tok1->comment) { + // a comment is replaced by one space character + if (tok1->previous) + tok1->previous->whitespaceahead = true; deleteToken(tok1); + } } } @@ -1381,8 +1792,9 @@ std::string simplecpp::TokenList::readUntil(Stream &stream, const Location &loca ret.erase(ret.size()-1U); backslash = (next == '\r'); update_ch = false; - } else if (next == '\\') + } else if (next == '\\') { update_ch = !update_ch; + } ret += next; } while (next == '\\'); if (update_ch) @@ -1393,11 +1805,12 @@ std::string simplecpp::TokenList::readUntil(Stream &stream, const Location &loca if (!stream.good() || ch != end) { clear(); if (outputList) { - Output err(files); - err.type = Output::SYNTAX_ERROR; - err.location = location; - err.msg = std::string("No pair for character (") + start + "). Can't process file. File is either invalid or unicode, which is currently not supported."; - outputList->push_back(err); + Output err{ + Output::SYNTAX_ERROR, + location, + std::string("No pair for character (") + start + "). Can't process file. File is either invalid or unicode, which is currently not supported." + }; + outputList->emplace_back(std::move(err)); } return ""; } @@ -1405,34 +1818,6 @@ std::string simplecpp::TokenList::readUntil(Stream &stream, const Location &loca return ret; } -std::string simplecpp::TokenList::lastLine(int maxsize) const -{ - std::string ret; - int count = 0; - for (const Token *tok = cback(); ; tok = tok->previous) { - if (!sameline(tok, cback())) { - break; - } - if (tok->comment) - continue; - if (++count > maxsize) - return ""; - if (!ret.empty()) - ret += ' '; - // add tokens in reverse for performance reasons - if (tok->str()[0] == '\"') - ret += "%rts%"; // %str% - else if (tok->number) - ret += "%mun%"; // %num% - else { - ret += tok->str(); - std::reverse(ret.end() - tok->str().length(), ret.end()); - } - } - std::reverse(ret.begin(), ret.end()); - return ret; -} - const simplecpp::Token* simplecpp::TokenList::lastLineTok(int maxsize) const { const Token* prevTok = nullptr; @@ -1449,10 +1834,12 @@ const simplecpp::Token* simplecpp::TokenList::lastLineTok(int maxsize) const return prevTok; } -bool simplecpp::TokenList::isLastLinePreprocessor(int maxsize) const +const simplecpp::Token* simplecpp::TokenList::isLastLinePreprocessor(int maxsize) const { const Token * const prevTok = lastLineTok(maxsize); - return prevTok && prevTok->op == '#'; + if (prevTok && prevTok->op == '#') + return prevTok; + return nullptr; } unsigned int simplecpp::TokenList::fileIndex(const std::string &filename) @@ -1461,19 +1848,35 @@ unsigned int simplecpp::TokenList::fileIndex(const std::string &filename) if (files[i] == filename) return i; } - files.push_back(filename); + files.emplace_back(filename); return files.size() - 1U; } +const std::string& simplecpp::TokenList::file(const Location& loc) const +{ + static const std::string s_emptyFileName; + return loc.fileIndex < files.size() ? files[loc.fileIndex] : s_emptyFileName; +} + namespace simplecpp { class Macro; using MacroMap = std::unordered_map; + /** Set the location of all tokens */ + static void setLocation(TokenList &tokens, const Location &loc) + { + for (Token *tok = tokens.front(); tok; tok = tok->next) + tok->location = loc; + } + class Macro { public: - explicit Macro(std::vector &f) : nameTokDef(nullptr), valueToken(nullptr), endToken(nullptr), files(f), tokenListDefine(f), variadic(false), variadicOpt(false), optExpandValue(nullptr), optNoExpandValue(nullptr), valueDefinedInCode_(false) {} + explicit Macro(std::vector &f) : nameTokDef(nullptr), valueToken(nullptr), endToken(nullptr), files(f), tokenListDefine(f), variadic(false), variadicOpt(false), valueDefinedInCode_(false) {} + /** + * @throws std::runtime_error thrown on bad macro syntax + */ Macro(const Token *tok, std::vector &f) : nameTokDef(nullptr), files(f), tokenListDefine(f), valueDefinedInCode_(true) { if (sameline(tok->previousSkipComments(), tok)) throw std::runtime_error("bad macro syntax"); @@ -1490,8 +1893,11 @@ namespace simplecpp { throw std::runtime_error("bad macro syntax"); } + /** + * @throws std::runtime_error thrown on bad macro syntax + */ Macro(const std::string &name, const std::string &value, std::vector &f) : nameTokDef(nullptr), files(f), tokenListDefine(f), valueDefinedInCode_(false) { - const std::string def(name + ' ' + value); + const std::string def(name + ' ' + value + '\n'); StdCharBufStream stream(reinterpret_cast(def.data()), def.size()); tokenListDefine.readfile(stream); if (!parseDefine(tokenListDefine.cfront())) @@ -1499,7 +1905,12 @@ namespace simplecpp { } Macro(const Macro &other) : nameTokDef(nullptr), files(other.files), tokenListDefine(other.files), valueDefinedInCode_(other.valueDefinedInCode_) { - *this = other; + // TODO: remove the try-catch - see #537 + // avoid bugprone-exception-escape clang-tidy warning + try { + *this = other; + } + catch (const Error&) {} // NOLINT(bugprone-empty-catch) } ~Macro() { @@ -1511,8 +1922,9 @@ namespace simplecpp { if (this != &other) { files = other.files; valueDefinedInCode_ = other.valueDefinedInCode_; - if (other.tokenListDefine.empty()) + if (other.tokenListDefine.empty()) { parseDefine(other.nameTokDef); + } else { tokenListDefine = other.tokenListDefine; parseDefine(tokenListDefine.cfront()); @@ -1526,19 +1938,51 @@ namespace simplecpp { return valueDefinedInCode_; } + /** + * Copy a macro call "NAME( ... )" to a tokenlist with no line breaks: + * all tokens get the location of the name. + * @param dest destination tokenlist + * @param nameTok the macro name, followed by '(' + * @return the token after the ')', or nullptr if the call is not terminated + * @throws invalidDirectiveAsMacroParameter if there is a preprocessor directive in the arguments + */ + static const Token *copyMacroCall(TokenList &dest, const Token *nameTok) { + const Location &loc = nameTok->location; + dest.push_back(new Token(nameTok->str(), loc, nameTok->whitespaceahead)); + const Token *tok = nameTok->next; + dest.push_back(new Token(tok->str(), loc, tok->whitespaceahead)); + tok = tok->next; + int par = 1; + while (tok && par > 0) { + if (tok->op == '(') + ++par; + else if (tok->op == ')') + --par; + else if (tok->op == '#' && !sameline(tok->previous, tok)) + throw invalidDirectiveAsMacroParameter(tok->location); + dest.push_back(new Token(tok->str(), loc, tok->whitespaceahead)); + tok = tok->next; + } + return tok; + } + /** * Expand macro. This will recursively expand inner macros. * @param output destination tokenlist * @param rawtok macro token * @param macros list of macros * @param inputFiles the input files + * @param outputList destination for warnings, may be nullptr * @return token after macro - * @throw Can throw wrongNumberOfParameters or invalidHashHash + * @throws Error thrown on missing or invalid preprocessor directives + * @throws wrongNumberOfParameters thrown on invalid number of parameters + * @throws invalidHashHash thrown on invalid ## usage */ - const Token * expand(TokenList * const output, + const Token * expand(TokenList & output, const Token * rawtok, const MacroMap ¯os, - std::vector &inputFiles) const { + std::vector &inputFiles, + OutputList *outputList) const { std::set expandedmacros; #ifdef SIMPLECPP_DEBUG_MACRO_EXPANSION @@ -1548,28 +1992,13 @@ namespace simplecpp { TokenList output2(inputFiles); if (functionLike() && rawtok->next && rawtok->next->op == '(') { - // Copy macro call to a new tokenlist with no linebreaks const Token * const rawtok1 = rawtok; TokenList rawtokens2(inputFiles); - rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); - rawtok = rawtok->next; - rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); - rawtok = rawtok->next; - int par = 1; - while (rawtok && par > 0) { - if (rawtok->op == '(') - ++par; - else if (rawtok->op == ')') - --par; - else if (rawtok->op == '#' && !sameline(rawtok->previous, rawtok)) - throw Error(rawtok->location, "it is invalid to use a preprocessor directive as macro parameter"); - rawtokens2.push_back(new Token(rawtok->str(), rawtok1->location, rawtok->whitespaceahead)); - rawtok = rawtok->next; - } - if (expand(&output2, rawtok1->location, rawtokens2.cfront(), macros, expandedmacros)) + rawtok = copyMacroCall(rawtokens2, rawtok); + if (expand(output2, rawtok1->location, rawtokens2.cfront(), macros, expandedmacros, outputList)) rawtok = rawtok1->next; } else { - rawtok = expand(&output2, rawtok->location, rawtok, macros, expandedmacros); + rawtok = expand(output2, rawtok->location, rawtok, macros, expandedmacros, outputList); } while (output2.cback() && rawtok) { unsigned int par = 0; @@ -1579,21 +2008,23 @@ namespace simplecpp { if (par==0) break; --par; - } else if (macro2tok->op == ')') + } else if (macro2tok->op == ')') { ++par; + } macro2tok = macro2tok->previous; } if (macro2tok) { // macro2tok->op == '(' macro2tok = macro2tok->previous; expandedmacros.insert(name()); - } else if (rawtok->op == '(') + } else if (rawtok->op == '(') { macro2tok = output2.back(); + } if (!macro2tok || !macro2tok->name) break; if (output2.cfront() != output2.cback() && macro2tok->str() == this->name()) break; const MacroMap::const_iterator macro = macros.find(macro2tok->str()); - if (macro == macros.end() || !macro->second.functionLike()) + if (macro == macros.end() || !macro->second.functionLike() || macro2tok->isExpandedFrom(¯o->second)) break; TokenList rawtokens2(inputFiles); const Location loc(macro2tok->location); @@ -1607,8 +2038,9 @@ namespace simplecpp { const Token *rawtok2 = rawtok; for (; rawtok2; rawtok2 = rawtok2->next) { rawtokens2.push_back(new Token(rawtok2->str(), loc)); - if (rawtok2->op == '(') + if (rawtok2->op == '(') { ++par; + } else if (rawtok2->op == ')') { if (par <= 1U) break; @@ -1617,11 +2049,11 @@ namespace simplecpp { } if (!rawtok2 || par != 1U) break; - if (macro->second.expand(&output2, rawtok->location, rawtokens2.cfront(), macros, expandedmacros) != nullptr) + if (macro->second.expand(output2, rawtok->location, rawtokens2.cfront(), macros, expandedmacros, outputList) != nullptr) break; rawtok = rawtok2->next; } - output->takeTokens(output2); + output.takeTokens(output2); return rawtok; } @@ -1655,6 +2087,11 @@ namespace simplecpp { const std::string what; }; + struct invalidDirectiveAsMacroParameter : public Error { + explicit invalidDirectiveAsMacroParameter(const Location &loc) + : Error(loc, "it is invalid to use a preprocessor directive as macro parameter") {} + }; + /** Struct that is thrown when macro is expanded with wrong number of parameters */ struct wrongNumberOfParameters : public Error { wrongNumberOfParameters(const Location &loc, const std::string ¯oName) : Error(loc, "Wrong number of parameters for macro \'" + macroName + "\'.") {} @@ -1667,28 +2104,28 @@ namespace simplecpp { } invalidHashHash(const Location &loc, const std::string ¯oName, const std::string &message) - : Error(loc, format(macroName, message)) { } + : Error(loc, format(macroName, message)) {} static inline invalidHashHash unexpectedToken(const Location &loc, const std::string ¯oName, const Token *tokenA) { - return invalidHashHash(loc, macroName, "Unexpected token '"+ tokenA->str()+"'"); + return {loc, macroName, "Unexpected token '"+ tokenA->str()+"'"}; } static inline invalidHashHash cannotCombine(const Location &loc, const std::string ¯oName, const Token *tokenA, const Token *tokenB) { - return invalidHashHash(loc, macroName, "Combining '"+ tokenA->str()+ "' and '"+ tokenB->str() + "' yields an invalid token."); + return {loc, macroName, "Combining '"+ tokenA->str()+ "' and '"+ tokenB->str() + "' yields an invalid token."}; } static inline invalidHashHash unexpectedNewline(const Location &loc, const std::string ¯oName) { - return invalidHashHash(loc, macroName, "Unexpected newline"); + return {loc, macroName, "Unexpected newline"}; } static inline invalidHashHash universalCharacterUB(const Location &loc, const std::string ¯oName, const Token* tokenA, const std::string& strAB) { - return invalidHashHash(loc, macroName, "Combining '\\"+ tokenA->str()+ "' and '"+ strAB.substr(tokenA->str().size()) + "' yields universal character '\\" + strAB + "'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4."); + return {loc, macroName, "Combining '\\"+ tokenA->str()+ "' and '"+ strAB.substr(tokenA->str().size()) + "' yields universal character '\\" + strAB + "'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4."}; } }; private: /** Create new token where Token::macro is set for replaced tokens */ Token *newMacroToken(const TokenString &str, const Location &loc, bool replaced, const Token *expandedFromToken=nullptr) const { - Token *tok = new Token(str,loc); + auto *tok = new Token(str,loc); if (replaced) tok->macro = nameTokDef->str(); if (expandedFromToken) @@ -1696,11 +2133,16 @@ namespace simplecpp { return tok; } + /** + * @throws Error thrown in case of __VA_OPT__ issues + */ bool parseDefine(const Token *nametoken) { nameTokDef = nametoken; variadic = false; variadicOpt = false; + delete optExpandValue; optExpandValue = nullptr; + delete optNoExpandValue; optNoExpandValue = nullptr; if (!nameTokDef) { valueToken = endToken = nullptr; @@ -1717,12 +2159,12 @@ namespace simplecpp { argtok->next && argtok->next->op == ')') { variadic = true; if (!argtok->previous->name) - args.push_back("__VA_ARGS__"); + args.emplace_back("__VA_ARGS__"); argtok = argtok->next; // goto ')' break; } if (argtok->op != ',') - args.push_back(argtok->str()); + args.emplace_back(argtok->str()); argtok = argtok->next; } if (!sameline(nametoken, argtok)) { @@ -1795,34 +2237,62 @@ namespace simplecpp { return ~0U; } + /** + * Find the arguments of a call of this macro. + * @param nameTokInst the macro name in the call + * @param calledInDefine the call is in a #define, so the arguments must be on the same line + * @return the tokens that delimit the arguments: the '(', each ',' between two arguments and the ')'. + * Empty when the macro is not function-like. + * @throws wrongNumberOfParameters if a function-like macro has the wrong number of arguments. + * This includes the case where the name is not followed by '(' (or is the last token), + * so the caller must check for that first if it is not an error. + */ std::vector getMacroParameters(const Token *nameTokInst, bool calledInDefine) const { - if (!nameTokInst->next || nameTokInst->next->op != '(' || !functionLike()) - return std::vector(); + if (!functionLike()) + return {}; + std::vector parametertokens; + if (nameTokInst->next && nameTokInst->next->op == '(') + parametertokens = findArgumentDelimiters(nameTokInst, calledInDefine); + + if (variadic) { + if (parametertokens.size() < args.size()) + throw wrongNumberOfParameters(nameTokInst->location, name()); + } else { + if (parametertokens.size() != args.size() + (args.empty() ? 2U : 1U)) + throw wrongNumberOfParameters(nameTokInst->location, name()); + } + return parametertokens; + } + /** The '(', each ',' between two arguments and the ')' of the call. nameTokInst is followed by '('. */ + std::vector findArgumentDelimiters(const Token *nameTokInst, bool calledInDefine) const { std::vector parametertokens; - parametertokens.push_back(nameTokInst->next); + parametertokens.emplace_back(nameTokInst->next); unsigned int par = 0U; for (const Token *tok = nameTokInst->next->next; calledInDefine ? sameline(tok, nameTokInst) : (tok != nullptr); tok = tok->next) { - if (tok->op == '(') + if (tok->op == '(') { ++par; + } else if (tok->op == ')') { if (par == 0U) { - parametertokens.push_back(tok); + parametertokens.emplace_back(tok); break; } --par; - } else if (par == 0U && tok->op == ',' && (!variadic || parametertokens.size() < args.size())) - parametertokens.push_back(tok); + } else if (par == 0U && tok->op == ',' && (!variadic || parametertokens.size() < args.size())) { + parametertokens.emplace_back(tok); + } } return parametertokens; } - const Token *appendTokens(TokenList *tokens, + const Token *appendTokens(TokenList &tokens, const Location &rawloc, const Token * const lpar, const MacroMap ¯os, const std::set &expandedmacros, - const std::vector ¶metertokens) const { + const std::vector ¶metertokens, + OutputList *outputList) const { if (!lpar || lpar->op != '(') return nullptr; unsigned int par = 0; @@ -1830,18 +2300,19 @@ namespace simplecpp { while (sameline(lpar, tok)) { if (tok->op == '#' && sameline(tok,tok->next) && tok->next->op == '#' && sameline(tok,tok->next->next)) { // A##B => AB - tok = expandHashHash(tokens, rawloc, tok, macros, expandedmacros, parametertokens, false); + tok = expandHashHash(tokens, rawloc, tok, macros, expandedmacros, parametertokens, outputList, false); } else if (tok->op == '#' && sameline(tok, tok->next) && tok->next->op != '#') { - tok = expandHash(tokens, rawloc, tok, expandedmacros, parametertokens); + tok = expandHash(tokens, rawloc, tok, expandedmacros, parametertokens, outputList); } else { - if (!expandArg(tokens, tok, rawloc, macros, expandedmacros, parametertokens)) { - tokens->push_back(new Token(*tok)); + if (!expandArg(tokens, tok, rawloc, macros, expandedmacros, parametertokens, outputList)) { + tokens.push_back(new Token(*tok)); if (tok->macro.empty() && (par > 0 || tok->str() != "(")) - tokens->back()->macro = name(); + tokens.back()->macro = name(); } - if (tok->op == '(') + if (tok->op == '(') { ++par; + } else if (tok->op == ')') { --par; if (par == 0U) @@ -1850,56 +2321,56 @@ namespace simplecpp { tok = tok->next; } } - for (Token *tok2 = tokens->front(); tok2; tok2 = tok2->next) - tok2->location = lpar->location; + setLocation(tokens, lpar->location); return sameline(lpar,tok) ? tok : nullptr; } - const Token * expand(TokenList * const output, const Location &loc, const Token * const nameTokInst, const MacroMap ¯os, std::set expandedmacros) const { + /** + * Expand the built-in macros __FILE__, __LINE__ and __COUNTER__. + * The usage must already be added to usageList, __COUNTER__ is the number of earlier usages. + * @param output destination tokenlist + * @param loc location of the usage + * @param nameTokInst the macro name + * @return true if the macro is built-in and its value is added to output + */ + bool expandBuiltin(TokenList &output, const Location &loc, const Token *nameTokInst) const { + std::string value; + if (nameTokInst->str() == "__FILE__") + value = '\"' + output.file(loc) + '\"'; + else if (nameTokInst->str() == "__LINE__") + value = toString(loc.line); + else if (nameTokInst->str() == "__COUNTER__") + value = toString(usageList.size() - 1U); + else + return false; + output.push_back(new Token(value, loc)); + output.back()->macro = nameTokInst->str(); + return true; + } + + const Token * expand(TokenList & output, const Location &loc, const Token * const nameTokInst, const MacroMap ¯os, std::set expandedmacros, OutputList *outputList) const { expandedmacros.insert(nameTokInst->str()); #ifdef SIMPLECPP_DEBUG_MACRO_EXPANSION std::cout << " expand " << name() << " " << locstring(defineLocation()) << std::endl; #endif - usageList.push_back(loc); + usageList.emplace_back(loc); - if (nameTokInst->str() == "__FILE__") { - output->push_back(new Token('\"'+loc.file()+'\"', loc)); - return nameTokInst->next; - } - if (nameTokInst->str() == "__LINE__") { - output->push_back(new Token(toString(loc.line), loc)); - return nameTokInst->next; - } - if (nameTokInst->str() == "__COUNTER__") { - output->push_back(new Token(toString(usageList.size()-1U), loc)); + if (expandBuiltin(output, loc, nameTokInst)) return nameTokInst->next; - } const bool calledInDefine = (loc.fileIndex != nameTokInst->location.fileIndex || loc.line < nameTokInst->location.line); - std::vector parametertokens1(getMacroParameters(nameTokInst, calledInDefine)); - - if (functionLike()) { - // No arguments => not macro expansion - if (nameTokInst->next && nameTokInst->next->op != '(') { - output->push_back(new Token(nameTokInst->str(), loc)); - return nameTokInst->next; - } - - // Parse macro-call - if (variadic) { - if (parametertokens1.size() < args.size()) { - throw wrongNumberOfParameters(nameTokInst->location, name()); - } - } else { - if (parametertokens1.size() != args.size() + (args.empty() ? 2U : 1U)) - throw wrongNumberOfParameters(nameTokInst->location, name()); - } + // No arguments => not macro expansion + if (functionLike() && nameTokInst->next && nameTokInst->next->op != '(') { + output.push_back(new Token(nameTokInst->str(), loc)); + return nameTokInst->next; } + std::vector parametertokens1(getMacroParameters(nameTokInst, calledInDefine)); + // If macro call uses __COUNTER__ then expand that first TokenList tokensparams(files); std::vector parametertokens2; @@ -1914,19 +2385,20 @@ namespace simplecpp { const MacroMap::const_iterator m = macros.find("__COUNTER__"); - if (!counter || m == macros.end()) + if (!counter || m == macros.end()) { parametertokens2.swap(parametertokens1); + } else { const Macro &counterMacro = m->second; unsigned int par = 0; for (const Token *tok = parametertokens1[0]; tok && par < parametertokens1.size(); tok = tok->next) { if (tok->str() == "__COUNTER__") { tokensparams.push_back(new Token(toString(counterMacro.usageList.size()), tok->location)); - counterMacro.usageList.push_back(tok->location); + counterMacro.usageList.emplace_back(tok->location); } else { tokensparams.push_back(new Token(*tok)); if (tok == parametertokens1[par]) { - parametertokens2.push_back(tokensparams.cback()); + parametertokens2.emplace_back(tokensparams.cback()); par++; } } @@ -1934,7 +2406,8 @@ namespace simplecpp { } } - Token * const output_end_1 = output->back(); + // NOLINTNEXTLINE(misc-const-correctness) - technically correct but used to access non-const data + Token * const output_end_1 = output.back(); const Token *valueToken2; const Token *endToken2; @@ -1959,23 +2432,23 @@ namespace simplecpp { throw invalidHashHash::unexpectedNewline(tok->location, name()); if (variadic && tok->op == ',' && tok->next->next->next->str() == args.back()) { Token *const comma = newMacroToken(tok->str(), loc, isReplaced(expandedmacros), tok); - output->push_back(comma); - tok = expandToken(output, loc, tok->next->next->next, macros, expandedmacros, parametertokens2); - if (output->back() == comma) - output->deleteToken(comma); + output.push_back(comma); + tok = expandToken(output, loc, tok->next->next->next, macros, expandedmacros, parametertokens2, outputList); + if (output.back() == comma) + output.deleteToken(comma); continue; } TokenList new_output(files); - if (!expandArg(&new_output, tok, parametertokens2)) - output->push_back(newMacroToken(tok->str(), loc, isReplaced(expandedmacros), tok)); + if (!expandArg(new_output, tok, parametertokens2)) + output.push_back(newMacroToken(tok->str(), loc, isReplaced(expandedmacros), tok)); else if (new_output.empty()) // placemarker token - output->push_back(newMacroToken("", loc, isReplaced(expandedmacros))); + output.push_back(newMacroToken("", loc, isReplaced(expandedmacros))); else for (const Token *tok2 = new_output.cfront(); tok2; tok2 = tok2->next) - output->push_back(newMacroToken(tok2->str(), loc, isReplaced(expandedmacros), tok2)); + output.push_back(newMacroToken(tok2->str(), loc, isReplaced(expandedmacros), tok2)); tok = tok->next; } else { - tok = expandToken(output, loc, tok, macros, expandedmacros, parametertokens2); + tok = expandToken(output, loc, tok, macros, expandedmacros, parametertokens2, outputList); } continue; } @@ -1988,33 +2461,39 @@ namespace simplecpp { } if (numberOfHash == 4 && tok->next->location.col + 1 == tok->next->next->location.col) { // # ## # => ## - output->push_back(newMacroToken("##", loc, isReplaced(expandedmacros))); + output.push_back(newMacroToken("##", loc, isReplaced(expandedmacros))); tok = hashToken; continue; } if (numberOfHash >= 2 && tok->location.col + 1 < tok->next->location.col) { - output->push_back(new Token(*tok)); + output.push_back(new Token(*tok)); tok = tok->next; continue; } tok = tok->next; if (tok == endToken2) { - output->push_back(new Token(*tok->previous)); + if (tok) { + output.push_back(new Token(*tok->previous)); + } + else { + output.push_back(new Token(*nameTokInst)); + output.back()->setstr("\"\""); + } break; } if (tok->op == '#') { // A##B => AB - tok = expandHashHash(output, loc, tok->previous, macros, expandedmacros, parametertokens2); + tok = expandHashHash(output, loc, tok->previous, macros, expandedmacros, parametertokens2, outputList); } else { // #123 => "123" - tok = expandHash(output, loc, tok->previous, expandedmacros, parametertokens2); + tok = expandHash(output, loc, tok->previous, expandedmacros, parametertokens2, outputList); } } if (!functionLike()) { - for (Token *tok = output_end_1 ? output_end_1->next : output->front(); tok; tok = tok->next) { + for (Token *tok = output_end_1 ? output_end_1->next : output.front(); tok; tok = tok->next) { tok->macro = nameTokInst->str(); } } @@ -2022,59 +2501,75 @@ namespace simplecpp { if (!parametertokens1.empty()) parametertokens1.swap(parametertokens2); - return functionLike() ? parametertokens2.back()->next : nameTokInst->next; - } + const Token * const callEnd = functionLike() ? parametertokens2.back() : nameTokInst; - const Token *recursiveExpandToken(TokenList *output, TokenList &temp, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens) const { - if (!(temp.cback() && temp.cback()->name && tok->next && tok->next->op == '(')) { - output->takeTokens(temp); - return tok->next; - } + // Whitespace at the end of the replacement list does not count. + // The expansion is followed by the whitespace after the macro call. + if (output.back() && output.back() != output_end_1) + output.back()->whitespaceahead = callEnd->whitespaceahead; - if (!sameline(tok, tok->next)) { - output->takeTokens(temp); - return tok->next; - } + return callEnd->next; + } + /** Returns the macro to expand when the last token of @p temp is the name of a + * function-like macro and the tokens after @p tok supply its arguments; nullptr otherwise */ + static const Macro *rescanMacro(const TokenList &temp, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros) { + if (!temp.cback() || !temp.cback()->name || !sameline(tok, tok->next) || tok->next->op != '(') + return nullptr; const MacroMap::const_iterator it = macros.find(temp.cback()->str()); - if (it == macros.end() || expandedmacros.find(temp.cback()->str()) != expandedmacros.end()) { - output->takeTokens(temp); - return tok->next; - } + if (it == macros.end() || expandedmacros.find(temp.cback()->str()) != expandedmacros.end()) + return nullptr; + if (!it->second.functionLike() || temp.cback()->isExpandedFrom(&it->second)) + return nullptr; + return &it->second; + } - const Macro &calledMacro = it->second; - if (!calledMacro.functionLike()) { - output->takeTokens(temp); - return tok->next; + /** Expand while the expansion result in @p temp ends with the name of a + * function-like macro whose arguments are supplied by the tokens after @p tok. + * Each round consumes that macro call from the token stream. The @p gatherCall + * callback copies the "( ... )" tokens that follow @p lpar into its output list + * and returns the matching ')', or nullptr when no complete call is available. + * Returns the first token that was not consumed (tok->next when no call was + * expanded). */ + template + const Token *expandTrailingCalls(TokenList &output, TokenList &temp, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, OutputList *outputList, GatherCall gatherCall) const { + while (const Macro * const calledMacro = rescanMacro(temp, tok, macros, expandedmacros)) { + TokenList temp2(files); + temp2.push_back(new Token(temp.cback()->str(), tok->next->location)); + + const Token * const closingPar = gatherCall(temp2, tok->next); + if (!closingPar) + break; + output.takeTokens(temp); + output.deleteToken(output.back()); + calledMacro->expand(temp, loc, temp2.cfront(), macros, expandedmacros, outputList); + tok = closingPar; } + output.takeTokens(temp); + return tok->next; + } - TokenList temp2(files); - temp2.push_back(new Token(temp.cback()->str(), tok->location)); - - const Token * const tok2 = appendTokens(&temp2, loc, tok->next, macros, expandedmacros, parametertokens); - if (!tok2) - return tok->next; - output->takeTokens(temp); - output->deleteToken(output->back()); - calledMacro.expand(output, loc, temp2.cfront(), macros, expandedmacros); - return tok2->next; + const Token *recursiveExpandToken(TokenList &output, TokenList &temp, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { + return expandTrailingCalls(output, temp, loc, tok, macros, expandedmacros, outputList, [&](TokenList &temp2, const Token *lpar) { + return appendTokens(temp2, loc, lpar, macros, expandedmacros, parametertokens, outputList); + }); } - const Token *expandToken(TokenList *output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens) const { + const Token *expandToken(TokenList &output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { // Not name.. if (!tok->name) { - output->push_back(newMacroToken(tok->str(), loc, true, tok)); + output.push_back(newMacroToken(tok->str(), loc, true, tok)); return tok->next; } // Macro parameter.. { TokenList temp(files); - if (expandArg(&temp, tok, loc, macros, expandedmacros, parametertokens)) { - if (tok->str() == "__VA_ARGS__" && temp.empty() && output->cback() && output->cback()->str() == "," && + if (expandArg(temp, tok, loc, macros, expandedmacros, parametertokens, outputList)) { + if (tok->str() == "__VA_ARGS__" && temp.empty() && output.cback() && output.cback()->str() == "," && tok->nextSkipComments() && tok->nextSkipComments()->str() == ")") - output->deleteToken(output->back()); - return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros, parametertokens); + output.deleteToken(output.back()); + return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros, parametertokens, outputList); } } @@ -2087,67 +2582,123 @@ namespace simplecpp { const Macro &calledMacro = it->second; if (!calledMacro.functionLike()) { TokenList temp(files); - calledMacro.expand(&temp, loc, tok, macros, expandedmacros); - return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros2, parametertokens); + calledMacro.expand(temp, loc, tok, macros, expandedmacros, outputList); + return recursiveExpandToken(output, temp, loc, tok, macros, expandedmacros2, parametertokens, outputList); } if (!sameline(tok, tok->next)) { - output->push_back(newMacroToken(tok->str(), loc, true, tok)); + output.push_back(newMacroToken(tok->str(), loc, true, tok)); return tok->next; } TokenList tokens(files); tokens.push_back(new Token(*tok)); const Token * tok2 = nullptr; - if (tok->next->op == '(') - tok2 = appendTokens(&tokens, loc, tok->next, macros, expandedmacros, parametertokens); - else if (expandArg(&tokens, tok->next, loc, macros, expandedmacros, parametertokens)) { + if (tok->next->op == '(') { + tok2 = appendTokens(tokens, loc, tok->next, macros, expandedmacros, parametertokens, outputList); + } + else if (expandArg(tokens, tok->next, loc, macros, expandedmacros, parametertokens, outputList)) { tokens.front()->location = loc; if (tokens.cfront()->next && tokens.cfront()->next->op == '(') tok2 = tok->next; } if (!tok2) { - output->push_back(newMacroToken(tok->str(), loc, true, tok)); + output.push_back(newMacroToken(tok->str(), loc, true, tok)); return tok->next; } TokenList temp(files); - calledMacro.expand(&temp, loc, tokens.cfront(), macros, expandedmacros); - return recursiveExpandToken(output, temp, loc, tok2, macros, expandedmacros, parametertokens); - } - - if (tok->str() == DEFINED) { - const Token * const tok2 = tok->next; - const Token * const tok3 = tok2 ? tok2->next : nullptr; - const Token * const tok4 = tok3 ? tok3->next : nullptr; - const Token *defToken = nullptr; - const Token *lastToken = nullptr; - if (sameline(tok, tok4) && tok2->op == '(' && tok3->name && tok4->op == ')') { - defToken = tok3; - lastToken = tok4; - } else if (sameline(tok,tok2) && tok2->name) { - defToken = lastToken = tok2; - } - if (defToken) { - std::string macroName = defToken->str(); - if (defToken->next && defToken->next->op == '#' && defToken->next->next && defToken->next->next->op == '#' && defToken->next->next->next && defToken->next->next->next->name && sameline(defToken,defToken->next->next->next)) { - TokenList temp(files); - if (expandArg(&temp, defToken, parametertokens)) - macroName = temp.cback()->str(); - if (expandArg(&temp, defToken->next->next->next, parametertokens)) - macroName += temp.cback() ? temp.cback()->str() : ""; - else - macroName += defToken->next->next->next->str(); - lastToken = defToken->next->next->next; - } - const bool def = (macros.find(macroName) != macros.end()); - output->push_back(newMacroToken(def ? "1" : "0", loc, true)); - return lastToken->next; - } + calledMacro.expand(temp, loc, tokens.cfront(), macros, expandedmacros, outputList); + return recursiveExpandToken(output, temp, loc, tok2, macros, expandedmacros, parametertokens, outputList); } - output->push_back(newMacroToken(tok->str(), loc, true, tok)); + const Token *next = nullptr; + if (tok->str() == DEFINED && expandDefined(output, loc, tok, macros, parametertokens, next)) + return next; + + output.push_back(newMacroToken(tok->str(), loc, true, tok)); + if (it != macros.end()) + output.back()->markExpandedFrom(&it->second); return tok->next; } - bool expandArg(TokenList *output, const Token *tok, const std::vector ¶metertokens) const { + /** + * Find the operand of "defined" in a replacement list: "defined X", "defined(X)" or "defined X ## Y". + * @param tok the "defined" token + * @param first output: X + * @param second output: Y for "X ## Y", otherwise nullptr + * @param last output: the last token of the operand + * @return false if there is no operand + */ + static bool findDefinedOperand(const Token *tok, const Token *&first, const Token *&second, const Token *&last) { + const Token * const tok2 = tok->next; + const Token * const tok3 = tok2 ? tok2->next : nullptr; + const Token * const tok4 = tok3 ? tok3->next : nullptr; + first = last = second = nullptr; + if (sameline(tok, tok4) && tok2->op == '(' && tok3->name && tok4->op == ')') { + first = tok3; + last = tok4; + } else if (sameline(tok,tok2) && tok2->name) { + first = last = tok2; + } + if (!first) + return false; + if (first->next && first->next->op == '#' && first->next->next && first->next->next->op == '#' && first->next->next->next && first->next->next->next->name && sameline(first,first->next->next->next)) { + second = first->next->next->next; + last = second; + } + return true; + } + + /** + * The name of the macro that "defined" checks. + * @param first X in "defined X" + * @param second Y in "defined X ## Y", or nullptr + * @param argText for "X ## Y": gives the text of a parameter, and returns false if the token is not a parameter + */ + template + static std::string definedMacroName(const Token *first, const Token *second, ArgText argText) { + std::string macroName = first->str(); + if (second) { + std::string text; + if (argText(first, text)) + macroName = text; + if (argText(second, text)) + macroName += text; + else + macroName += second->str(); + } + return macroName; + } + + /** + * Expand "defined X" or "defined(X)" in the replacement list to 1 or 0. + * The operand can be a parameter, or two names pasted with ##. + * @param output destination tokenlist + * @param loc location for the result + * @param tok the "defined" token + * @param macros all macros + * @param parametertokens arguments of this macro call + * @param next output: the token after the operand + * @return false if there is no operand, then nothing is added to output + */ + bool expandDefined(TokenList &output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::vector ¶metertokens, const Token *&next) const { + const Token *first; + const Token *second; + const Token *last; + if (!findDefinedOperand(tok, first, second, last)) + return false; + TokenList temp(files); + const std::string macroName = definedMacroName(first, second, [&](const Token *argtok, std::string &text) { + if (!expandArg(temp, argtok, parametertokens)) + return false; + text = temp.cback() ? temp.cback()->str() : ""; + return true; + }); + const bool def = (macros.find(macroName) != macros.end()); + output.push_back(newMacroToken(def ? "1" : "0", loc, true)); + next = last->next; + return true; + } + + bool expandArg(TokenList &output, const Token *tok, const std::vector ¶metertokens) const { if (!tok->name) return false; @@ -2160,12 +2711,12 @@ namespace simplecpp { return true; for (const Token *partok = parametertokens[argnr]->next; partok != parametertokens[argnr + 1U]; partok = partok->next) - output->push_back(new Token(*partok)); + output.push_back(new Token(*partok)); return true; } - bool expandArg(TokenList *output, const Token *tok, const Location &loc, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens) const { + bool expandArg(TokenList &output, const Token *tok, const Location &loc, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { if (!tok->name) return false; const unsigned int argnr = getArgNum(tok->str()); @@ -2173,20 +2724,43 @@ namespace simplecpp { return false; if (variadic && argnr + 1U >= parametertokens.size()) // empty variadic parameter return true; - for (const Token *partok = parametertokens[argnr]->next; partok != parametertokens[argnr + 1U];) { + const Token * const argEnd = parametertokens[argnr + 1U]; + for (const Token *partok = parametertokens[argnr]->next; partok != argEnd;) { const MacroMap::const_iterator it = macros.find(partok->str()); if (it != macros.end() && !partok->isExpandedFrom(&it->second) && (partok->str() == name() || expandedmacros.find(partok->str()) == expandedmacros.end())) { std::set expandedmacros2(expandedmacros); // temporary amnesia to allow reexpansion of currently expanding macros during argument evaluation expandedmacros2.erase(name()); - partok = it->second.expand(output, loc, partok, macros, expandedmacros2); + TokenList temp(files); + partok = it->second.expand(temp, loc, partok, macros, std::move(expandedmacros2), outputList); + if (partok->op == '(' && temp.cback() && temp.cback()->name) { + // the expansion result may end with the name of a function-like + // macro whose arguments are supplied by the remaining argument tokens + std::set expandedmacros3(expandedmacros); + expandedmacros3.erase(name()); + partok = expandTrailingCalls(output, temp, loc, partok->previous, macros, expandedmacros3, outputList, [&](TokenList &temp2, const Token *lpar) -> const Token * { + unsigned int par = 0; + for (const Token *tok2 = lpar; tok2 != argEnd; tok2 = tok2->next) { + temp2.push_back(new Token(*tok2)); + if (tok2->op == '(') { + ++par; + } else if (tok2->op == ')') { + if (--par == 0U) + return tok2; + } + } + return nullptr; + }); + } else { + output.takeTokens(temp); + } } else { - output->push_back(newMacroToken(partok->str(), loc, isReplaced(expandedmacros), partok)); - output->back()->macro = partok->macro; + output.push_back(newMacroToken(partok->str(), loc, isReplaced(expandedmacros), partok)); + output.back()->macro = partok->macro; partok = partok->next; } } - if (tok->whitespaceahead && output->back()) - output->back()->whitespaceahead = true; + if (tok->whitespaceahead && output.back()) + output.back()->whitespaceahead = true; return true; } @@ -2199,20 +2773,11 @@ namespace simplecpp { * @param parametertokens parameters given when expanding this macro * @return token after the X */ - const Token *expandHash(TokenList *output, const Location &loc, const Token *tok, const std::set &expandedmacros, const std::vector ¶metertokens) const { + const Token *expandHash(TokenList &output, const Location &loc, const Token *tok, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList) const { TokenList tokenListHash(files); const MacroMap macros2; // temporarily bypass macro expansion - tok = expandToken(&tokenListHash, loc, tok->next, macros2, expandedmacros, parametertokens); - std::ostringstream ostr; - ostr << '\"'; - for (const Token *hashtok = tokenListHash.cfront(), *next; hashtok; hashtok = next) { - next = hashtok->next; - ostr << hashtok->str(); - if (next && hashtok->whitespaceahead) - ostr << ' '; - } - ostr << '\"'; - output->push_back(newMacroToken(escapeString(ostr.str()), loc, isReplaced(expandedmacros))); + tok = expandToken(tokenListHash, loc, tok->next, macros2, expandedmacros, parametertokens, outputList); + output.push_back(newMacroToken(stringify(tokenListHash, loc, name(), outputList), loc, isReplaced(expandedmacros))); return tok; } @@ -2228,122 +2793,154 @@ namespace simplecpp { * @param expandResult expand ## result i.e. "AB"? * @return token after B */ - const Token *expandHashHash(TokenList *output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, bool expandResult=true) const { - Token *A = output->back(); + const Token *expandHashHash(TokenList &output, const Location &loc, const Token *tok, const MacroMap ¯os, const std::set &expandedmacros, const std::vector ¶metertokens, OutputList *outputList, bool expandResult=true) const { + Token *A = output.back(); if (!A) throw invalidHashHash(tok->location, name(), "Missing first argument"); if (!sameline(tok, tok->next) || !sameline(tok, tok->next->next)) throw invalidHashHash::unexpectedNewline(tok->location, name()); - const bool canBeConcatenatedWithEqual = A->isOneOf("+-*/%&|^") || A->str() == "<<" || A->str() == ">>"; - const bool canBeConcatenatedStringOrChar = isStringLiteral_(A->str()) || isCharLiteral_(A->str()); - const bool unexpectedA = (!A->name && !A->number && !A->str().empty() && !canBeConcatenatedWithEqual && !canBeConcatenatedStringOrChar); - - Token * const B = tok->next->next; - if (!B->name && !B->number && B->op && !B->isOneOf("#=")) - throw invalidHashHash::unexpectedToken(tok->location, name(), B); - - if ((canBeConcatenatedWithEqual && B->op != '=') || - (!canBeConcatenatedWithEqual && B->op == '=')) - throw invalidHashHash::cannotCombine(tok->location, name(), A, B); + const bool canBeConcatenatedStringOrChar = isStringLiteral(A->str()) || isCharLiteral(A->str()); - // Superficial check; more in-depth would in theory be possible _after_ expandArg - if (canBeConcatenatedStringOrChar && (B->number || !B->name)) - throw invalidHashHash::cannotCombine(tok->location, name(), A, B); + const Token * const B = tok->next->next; + checkHashHashOperands(tok->location, name(), A, B); TokenList tokensB(files); const Token *nextTok = B->next; if (canBeConcatenatedStringOrChar) { - if (unexpectedA) - throw invalidHashHash::unexpectedToken(tok->location, name(), A); - // It seems clearer to handle this case separately even though the code is similar-ish, but we don't want to merge here. // TODO The question is whether the ## or varargs may still apply, and how to provoke? - if (expandArg(&tokensB, B, parametertokens)) { - for (Token *b = tokensB.front(); b; b = b->next) - b->location = loc; + if (expandArg(tokensB, B, parametertokens)) { + setLocation(tokensB, loc); } else { tokensB.push_back(new Token(*B)); tokensB.back()->location = loc; } - output->takeTokens(tokensB); + output.takeTokens(tokensB); } else { std::string strAB; const bool varargs = variadic && !args.empty() && B->str() == args[args.size()-1U]; - if (expandArg(&tokensB, B, parametertokens)) { - if (tokensB.empty()) + if (expandArg(tokensB, B, parametertokens)) { + if (tokensB.empty()) { strAB = A->str(); - else if (varargs && A->op == ',') + } + else if (varargs && A->op == ',') { strAB = ","; - else if (varargs && unexpectedA) - throw invalidHashHash::unexpectedToken(tok->location, name(), A); + } else { + checkPunctuatorPaste(tok->location, name(), A, tokensB.cfront()); strAB = A->str() + tokensB.cfront()->str(); tokensB.deleteToken(tokensB.front()); } } else { - if (unexpectedA) - throw invalidHashHash::unexpectedToken(tok->location, name(), A); + checkPunctuatorPaste(tok->location, name(), A, B); strAB = A->str() + B->str(); } - // producing universal character is undefined behavior - if (A->previous && A->previous->str() == "\\") { - if (strAB[0] == 'u' && strAB.size() == 5) - throw invalidHashHash::universalCharacterUB(tok->location, name(), A, strAB); - if (strAB[0] == 'U' && strAB.size() == 9) - throw invalidHashHash::universalCharacterUB(tok->location, name(), A, strAB); - } + checkUniversalCharacterPaste(tok->location, name(), A, strAB); - if (varargs && tokensB.empty() && tok->previous->str() == ",") - output->deleteToken(A); + if (varargs && tokensB.empty() && tok->previous->str() == ",") { + output.deleteToken(A); + } else if (strAB != "," && macros.find(strAB) == macros.end()) { A->setstr(strAB); - for (Token *b = tokensB.front(); b; b = b->next) - b->location = loc; - output->takeTokens(tokensB); + setLocation(tokensB, loc); + output.takeTokens(tokensB); } else if (sameline(B, nextTok) && sameline(B, nextTok->next) && nextTok->op == '#' && nextTok->next->op == '#') { TokenList output2(files); output2.push_back(new Token(strAB, tok->location)); - nextTok = expandHashHash(&output2, loc, nextTok, macros, expandedmacros, parametertokens); - output->deleteToken(A); - output->takeTokens(output2); + nextTok = expandHashHash(output2, loc, nextTok, macros, expandedmacros, parametertokens, outputList); + output.deleteToken(A); + output.takeTokens(output2); } else { - output->deleteToken(A); + output.deleteToken(A); TokenList tokens(files); tokens.push_back(new Token(strAB, tok->location)); // for function like macros, push the (...) if (tokensB.empty() && sameline(B,B->next) && B->next->op=='(') { const MacroMap::const_iterator it = macros.find(strAB); if (it != macros.end() && expandedmacros.find(strAB) == expandedmacros.end() && it->second.functionLike()) { - const Token * const tok2 = appendTokens(&tokens, loc, B->next, macros, expandedmacros, parametertokens); + const Token * const tok2 = appendTokens(tokens, loc, B->next, macros, expandedmacros, parametertokens, outputList); if (tok2) nextTok = tok2->next; } } if (expandResult) - expandToken(output, loc, tokens.cfront(), macros, expandedmacros, parametertokens); + expandToken(output, loc, tokens.cfront(), macros, expandedmacros, parametertokens, outputList); else - output->takeTokens(tokens); - for (Token *b = tokensB.front(); b; b = b->next) - b->location = loc; - output->takeTokens(tokensB); + output.takeTokens(tokens); + setLocation(tokensB, loc); + output.takeTokens(tokensB); } } return nextTok; } + /** A is a punctuator, not a name, number or literal. Pasting it is checked after B is expanded. */ + static bool isPunctuatorOperand(const Token *A) { + return !A->name && !A->number && !A->str().empty() && !isStringLiteral(A->str()) && !isCharLiteral(A->str()); + } + + /** + * Check the operands of A ## B before B is expanded. + * @param loc the location of the ## + * @param macroName the macro that is expanded + * @param A the token before ##, already expanded + * @param B the token after ## in the macro definition + * @throws invalidHashHash if A and B can not be pasted + */ + static void checkHashHashOperands(const Location &loc, const std::string ¯oName, const Token *A, const Token *B) { + if (!isPunctuatorOperand(A)) { + if (!B->name && !B->number && B->op && !B->isOneOf("#=")) + throw invalidHashHash::unexpectedToken(loc, macroName, B); + if (B->op == '=') + throw invalidHashHash::cannotCombine(loc, macroName, A, B); + } + + // Superficial check; more in-depth would in theory be possible _after_ expandArg + if ((isStringLiteral(A->str()) || isCharLiteral(A->str())) && (B->number || !B->name)) + throw invalidHashHash::cannotCombine(loc, macroName, A, B); + } + + /** + * When A is a punctuator, check that pasting A and B gives a valid token. + * @param loc the location of the ## + * @param macroName the macro that is expanded + * @param A the token before ## + * @param B the first token after ##, after expansion + * @throws invalidHashHash if the result is not a valid token + */ + static void checkPunctuatorPaste(const Location &loc, const std::string ¯oName, const Token *A, const Token *B) { + if (isPunctuatorOperand(A) && !isValidPunctuatorPaste(A->str() + B->str())) + throw invalidHashHash::cannotCombine(loc, macroName, A, B); + } + + /** + * Producing a universal character name with ## is undefined behavior. + * @param loc the location of the ## + * @param macroName the macro that is expanded + * @param A the token before ## + * @param strAB the pasted text + * @throws invalidHashHash if A follows a backslash and strAB is a universal character name + */ + static void checkUniversalCharacterPaste(const Location &loc, const std::string ¯oName, const Token *A, const std::string &strAB) { + if (!A->previous || A->previous->str() != "\\") + return; + if ((strAB[0] == 'u' && strAB.size() == 5) || (strAB[0] == 'U' && strAB.size() == 9)) + throw invalidHashHash::universalCharacterUB(loc, macroName, A, strAB); + } + static bool isReplaced(const std::set &expandedmacros) { // return true if size > 1 - std::set::const_iterator it = expandedmacros.begin(); - if (it == expandedmacros.end()) + auto it = expandedmacros.cbegin(); + if (it == expandedmacros.cend()) return false; ++it; - return (it != expandedmacros.end()); + return (it != expandedmacros.cend()); } /** name token in definition */ @@ -2374,8 +2971,8 @@ namespace simplecpp { bool variadicOpt; /** Expansion value for varadic macros with __VA_OPT__ expanded and discarded respectively */ - const TokenList *optExpandValue; - const TokenList *optNoExpandValue; + const TokenList *optExpandValue{}; + const TokenList *optNoExpandValue{}; /** was the value of this macro actually defined in the code? */ bool valueDefinedInCode_; @@ -2385,12 +2982,17 @@ namespace simplecpp { namespace simplecpp { #ifdef __CYGWIN__ + static bool startsWith(const std::string &s, const std::string &p) + { + return (s.size() >= p.size()) && std::equal(p.begin(), p.end(), s.begin()); + } + std::string convertCygwinToWindowsPath(const std::string &cygwinPath) { std::string windowsPath; std::string::size_type pos = 0; - if (cygwinPath.size() >= 11 && startsWith_(cygwinPath, "/cygdrive/")) { + if (cygwinPath.size() >= 11 && startsWith(cygwinPath, "/cygdrive/")) { const unsigned char driveLetter = cygwinPath[10]; if (std::isalpha(driveLetter)) { if (cygwinPath.size() == 11) { @@ -2415,21 +3017,26 @@ namespace simplecpp { return windowsPath; } #endif -} + bool isAbsolutePath(const std::string &path) + { #ifdef SIMPLECPP_WINDOWS -static bool isAbsolutePath(const std::string &path) -{ - if (path.length() >= 3 && path[0] > 0 && std::isalpha(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) - return true; - return path.length() > 1U && (path[0] == '/' || path[0] == '\\'); -} + // C:\\path\\file + // C:/path/file + if (path.length() >= 3 && std::isalpha(path[0]) && path[1] == ':' && (path[2] == '\\' || path[2] == '/')) + return true; + + // \\host\path\file + // //host/path/file + if (path.length() >= 2 && (path[0] == '\\' || path[0] == '/') && (path[1] == '\\' || path[1] == '/')) + return true; + + return false; #else -static bool isAbsolutePath(const std::string &path) -{ - return path.length() > 1U && path[0] == '/'; -} + return !path.empty() && path[0] == '/'; #endif + } +} namespace simplecpp { /** @@ -2506,17 +3113,19 @@ namespace simplecpp { } } -/** Evaluate sizeof(type) */ +/** Evaluate sizeof(type) + * @throws std::runtime_error thrown on missing arguments or invalid expression + */ static void simplifySizeof(simplecpp::TokenList &expr, const std::map &sizeOfType) { for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { if (tok->str() != "sizeof") continue; - simplecpp::Token *tok1 = tok->next; + const simplecpp::Token *tok1 = tok->next; if (!tok1) { throw std::runtime_error("missing sizeof argument"); } - simplecpp::Token *tok2 = tok1->next; + const simplecpp::Token *tok2 = tok1->next; if (!tok2) { throw std::runtime_error("missing sizeof argument"); } @@ -2531,7 +3140,7 @@ static void simplifySizeof(simplecpp::TokenList &expr, const std::mapnext) { + for (const simplecpp::Token *typeToken = tok1; typeToken != tok2; typeToken = typeToken->next) { if ((typeToken->str() == "unsigned" || typeToken->str() == "signed") && typeToken->next->name) continue; if (typeToken->str() == "*" && type.find('*') != std::string::npos) @@ -2556,7 +3165,7 @@ static void simplifySizeof(simplecpp::TokenList &expr, const std::map= "201703L"); + return std_ver.empty() || (std_ver >= "201703L"); } static bool isGnu(const simplecpp::DUI &dui) @@ -2574,6 +3183,10 @@ static std::string dirPath(const std::string& path, bool withTrailingSlash=true) } static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const std::string &sourcefile, const std::string &header, bool systemheader); + +/** Evaluate __has_include(include) + * @throws std::runtime_error thrown on missing arguments or invalid expression + */ static void simplifyHasInclude(simplecpp::TokenList &expr, const simplecpp::DUI &dui) { if (!isCpp17OrLater(dui) && !isGnu(dui)) @@ -2582,11 +3195,11 @@ static void simplifyHasInclude(simplecpp::TokenList &expr, const simplecpp::DUI for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { if (tok->str() != HAS_INCLUDE) continue; - simplecpp::Token *tok1 = tok->next; + const simplecpp::Token *tok1 = tok->next; if (!tok1) { throw std::runtime_error("missing __has_include argument"); } - simplecpp::Token *tok2 = tok1->next; + const simplecpp::Token *tok2 = tok1->next; if (!tok2) { throw std::runtime_error("missing __has_include argument"); } @@ -2600,11 +3213,11 @@ static void simplifyHasInclude(simplecpp::TokenList &expr, const simplecpp::DUI } } - const std::string &sourcefile = tok->location.file(); + const std::string &sourcefile = expr.file(tok->location); const bool systemheader = (tok1 && tok1->op == '<'); std::string header; if (systemheader) { - simplecpp::Token *tok3 = tok1->next; + const simplecpp::Token *tok3 = tok1->next; if (!tok3) { throw std::runtime_error("missing __has_include closing angular bracket"); } @@ -2615,7 +3228,7 @@ static void simplifyHasInclude(simplecpp::TokenList &expr, const simplecpp::DUI } } - for (simplecpp::Token *headerToken = tok1->next; headerToken != tok3; headerToken = headerToken->next) + for (const simplecpp::Token *headerToken = tok1->next; headerToken != tok3; headerToken = headerToken->next) header += headerToken->str(); } else { header = tok1->str().substr(1U, tok1->str().size() - 2U); @@ -2630,13 +3243,22 @@ static void simplifyHasInclude(simplecpp::TokenList &expr, const simplecpp::DUI } } -static const char * const altopData[] = {"and","or","bitand","bitor","compl","not","not_eq","xor"}; -static const std::set altop(&altopData[0], &altopData[8]); -static void simplifyName(simplecpp::TokenList &expr) +/** Evaluate name + * @throws std::runtime_error thrown on undefined function-like macro + */ +static void simplifyName(simplecpp::TokenList &expr, const simplecpp::DUI &dui) { + // In C++ and C23 the keywords true/false are 1/0 in #if expressions + const simplecpp::cstd_t cstd = simplecpp::getCStd(dui.std); + const bool hasTrueFalse = (cstd == simplecpp::CUnknown || cstd >= simplecpp::C23); + for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { if (tok->name) { - if (altop.find(tok->str()) != altop.end()) { + if (hasTrueFalse && (tok->str() == "true" || tok->str() == "false")) { + tok->setstr(tok->str() == "true" ? "1" : "0"); + continue; + } + if (isAlternativeOpName(tok)) { bool alt; if (tok->str() == "not" || tok->str() == "compl") { alt = isAlternativeUnaryOp(tok,tok->str()); @@ -2659,7 +3281,7 @@ static void simplifyName(simplecpp::TokenList &expr) * unsigned long long value, updating pos to point to the first * unused element of s. * Returns ULLONG_MAX if the result is not representable and - * throws if the above requirements were not possible to satisfy. + * @throws std::runtime_error thrown if the above requirements were not possible to satisfy. */ static unsigned long long stringToULLbounded( const std::string& s, @@ -2667,7 +3289,7 @@ static unsigned long long stringToULLbounded( int base = 0, std::ptrdiff_t minlen = 1, std::size_t maxlen = std::string::npos -) + ) { const std::string sub = s.substr(pos, maxlen); const char * const start = sub.c_str(); @@ -2679,34 +3301,6 @@ static unsigned long long stringToULLbounded( return value; } -/* Converts character literal (including prefix, but not ud-suffix) - * to long long value. - * - * Assumes ASCII-compatible single-byte encoded str for narrow literals - * and UTF-8 otherwise. - * - * For target assumes - * - execution character set encoding matching str - * - UTF-32 execution wide-character set encoding - * - requirements for __STDC_UTF_16__, __STDC_UTF_32__ and __STDC_ISO_10646__ satisfied - * - char16_t is 16bit wide - * - char32_t is 32bit wide - * - wchar_t is 32bit wide and unsigned - * - matching char signedness to host - * - matching sizeof(int) to host - * - * For host assumes - * - ASCII-compatible execution character set - * - * For host and target assumes - * - CHAR_BIT == 8 - * - two's complement - * - * Implements multi-character narrow literals according to GCC's behavior, - * except multi code unit universal character names are not supported. - * Multi-character wide literals are not supported. - * Limited support of universal character names for non-UTF-8 execution character set encodings. - */ long long simplecpp::characterLiteralToLL(const std::string& str) { // default is wide/utf32 @@ -2727,8 +3321,9 @@ long long simplecpp::characterLiteralToLL(const std::string& str) pos = 3; } else if (str.size() >= 2 && (str[0] == 'L' || str[0] == 'U') && str[1] == '\'') { pos = 2; - } else + } else { throw std::runtime_error("expected a character literal"); + } unsigned long long multivalue = 0; @@ -2900,9 +3495,27 @@ long long simplecpp::characterLiteralToLL(const std::string& str) return multivalue; } +static bool isFloatingPointLiteral(const std::string &str) +{ + const bool startsWithDotDigit = str.size() > 1U && str[0] == '.' && + std::isdigit(static_cast(str[1])); + if (!simplecpp::Token::isNumberLike(str) && !startsWithDotDigit) + return false; + + if (str.find('.') != std::string::npos) + return true; + + return str.find_first_of(isHex(str) ? "pP" : "eE") != std::string::npos; +} + +/** + * @throws std::runtime_error thrown on invalid literal + */ static void simplifyNumbers(simplecpp::TokenList &expr) { for (simplecpp::Token *tok = expr.front(); tok; tok = tok->next) { + if (isFloatingPointLiteral(tok->str())) + throw std::runtime_error("floating point literal in preprocessor expression"); if (tok->str().size() == 1U) continue; if (tok->str().compare(0,2,"0x") == 0) @@ -2922,16 +3535,22 @@ static void simplifyComments(simplecpp::TokenList &expr) } } +/** + * @throws std::runtime_error thrown on invalid literals, missing sizeof arguments or invalid expressions, + * missing __has_include() arguments or expressions, undefined function-like macros, invalid number literals + * @throws std::overflow_error thrown on overflow or division by zero + */ static long long evaluate(simplecpp::TokenList &expr, const simplecpp::DUI &dui, const std::map &sizeOfType) { simplifyComments(expr); simplifySizeof(expr, sizeOfType); simplifyHasInclude(expr, dui); - simplifyName(expr); + simplifyName(expr, dui); simplifyNumbers(expr); expr.constFold(); - // TODO: handle invalid expressions - return expr.cfront() && expr.cfront() == expr.cback() && expr.cfront()->number ? stringToLL(expr.cfront()->str()) : 0LL; + if (!expr.cfront() || expr.cfront() != expr.cback() || !expr.cfront()->number) + throw std::runtime_error("invalid expression"); + return stringToLL(expr.cfront()->str()); } static const simplecpp::Token *gotoNextLine(const simplecpp::Token *tok) @@ -2990,7 +3609,7 @@ static std::string openHeaderDirect(std::ifstream &f, const std::string &path) static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const std::string &sourcefile, const std::string &header, bool systemheader) { - if (isAbsolutePath(header)) + if (simplecpp::isAbsolutePath(header)) return openHeaderDirect(f, simplecpp::simplifyPath(header)); // prefer first to search the header relatively to source file if found, when not a system header @@ -3010,6 +3629,65 @@ static std::string openHeader(std::ifstream &f, const simplecpp::DUI &dui, const return ""; } +namespace { + struct FileID { +#ifdef _WIN32 + struct { + std::uint64_t VolumeSerialNumber; + struct { + std::uint64_t IdentifierHi; + std::uint64_t IdentifierLo; + } FileId; + } fileIdInfo; + + bool operator==(const FileID &that) const noexcept { + return fileIdInfo.VolumeSerialNumber == that.fileIdInfo.VolumeSerialNumber && + fileIdInfo.FileId.IdentifierHi == that.fileIdInfo.FileId.IdentifierHi && + fileIdInfo.FileId.IdentifierLo == that.fileIdInfo.FileId.IdentifierLo; + } +#else + dev_t dev; + ino_t ino; + + bool operator==(const FileID& that) const noexcept { + return dev == that.dev && ino == that.ino; + } +#endif + struct Hasher { + std::size_t operator()(const FileID &id) const { +#ifdef _WIN32 + return static_cast(id.fileIdInfo.FileId.IdentifierHi ^ id.fileIdInfo.FileId.IdentifierLo ^ + id.fileIdInfo.VolumeSerialNumber); +#else + return static_cast(id.dev) ^ static_cast(id.ino); +#endif + } + }; + }; +} + +struct simplecpp::FileDataCache::Impl +{ + void clear() + { + mIdMap.clear(); + } + + using id_map_type = std::unordered_map; + + id_map_type mIdMap; +}; + +simplecpp::FileDataCache::FileDataCache() + : mImpl(new Impl) +{} + +simplecpp::FileDataCache::~FileDataCache() = default; +simplecpp::FileDataCache::FileDataCache(FileDataCache &&) SIMPLECPP_NOEXCEPT = default; +simplecpp::FileDataCache &simplecpp::FileDataCache::operator=(simplecpp::FileDataCache &&) SIMPLECPP_NOEXCEPT = default; + +static bool getFileId(const std::string &path, FileID &id); + std::pair simplecpp::FileDataCache::tryload(FileDataCache::name_map_type::iterator &name_it, const simplecpp::DUI &dui, std::vector &filenames, simplecpp::OutputList *outputList) { const std::string &path = name_it->first; @@ -3018,26 +3696,25 @@ std::pair simplecpp::FileDataCache::tryload(FileDat if (!getFileId(path, fileId)) return {nullptr, false}; - const auto id_it = mIdMap.find(fileId); - if (id_it != mIdMap.end()) { + const auto id_it = mImpl->mIdMap.find(fileId); + if (id_it != mImpl->mIdMap.end()) { name_it->second = id_it->second; return {id_it->second, false}; } - std::ifstream f(path); - FileData *const data = new FileData {path, TokenList(f, filenames, path, outputList)}; + auto *const data = new FileData {path, TokenList(path, filenames, {}, outputList)}; if (dui.removeComments) data->tokens.removeComments(); name_it->second = data; - mIdMap.emplace(fileId, data); + mImpl->mIdMap.emplace(fileId, data); mData.emplace_back(data); return {data, true}; } -std::pair simplecpp::FileDataCache::get(const std::string &sourcefile, const std::string &header, const simplecpp::DUI &dui, bool systemheader, std::vector &filenames, simplecpp::OutputList *outputList) +std::pair simplecpp::FileDataCache::get_private(const std::string &sourcefile, const std::string &header, const simplecpp::DUI &dui, bool systemheader, std::vector &filenames, simplecpp::OutputList *outputList) { if (isAbsolutePath(header)) { auto ins = mNameMap.emplace(simplecpp::simplifyPath(header), nullptr); @@ -3083,15 +3760,46 @@ std::pair simplecpp::FileDataCache::get(const std:: return {nullptr, false}; } -bool simplecpp::FileDataCache::getFileId(const std::string &path, FileID &id) +std::pair simplecpp::FileDataCache::get(const std::string &sourcefile, const std::string &header, const simplecpp::DUI &dui, bool systemheader, std::vector &filenames, simplecpp::OutputList *outputList) { -#ifdef SIMPLECPP_WINDOWS - HANDLE hFile = CreateFileA(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + auto ret = get_private(sourcefile, header, dui, systemheader, filenames, outputList); + + if (mLoadCallback && ret.first) + mLoadCallback(*ret.first, ret.second); + + return ret; +} + +void simplecpp::FileDataCache::clear() +{ + mImpl->clear(); + mNameMap.clear(); + mData.clear(); +} + +static bool getFileId(const std::string &path, FileID &id) +{ +#ifdef _WIN32 + HANDLE hFile = CreateFileA(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hFile == INVALID_HANDLE_VALUE) return false; - const BOOL ret = GetFileInformationByHandleEx(hFile, FileIdInfo, &id.fileIdInfo, sizeof(id.fileIdInfo)); + BOOL ret = GetFileInformationByHandleEx(hFile, FileIdInfo, &id.fileIdInfo, sizeof(id.fileIdInfo)); + if (!ret) { + const DWORD err = GetLastError(); + if (err == ERROR_INVALID_PARAMETER || // encountered when using a non-NTFS filesystem e.g. exFAT + err == ERROR_NOT_SUPPORTED) // encountered on Windows Server Core (used as a Docker container) + { + BY_HANDLE_FILE_INFORMATION fileInfo; + ret = GetFileInformationByHandle(hFile, &fileInfo); + if (ret) { + id.fileIdInfo.VolumeSerialNumber = static_cast(fileInfo.dwVolumeSerialNumber); + id.fileIdInfo.FileId.IdentifierHi = static_cast(fileInfo.nFileIndexHigh); + id.fileIdInfo.FileId.IdentifierLo = static_cast(fileInfo.nFileIndexLow); + } + } + } CloseHandle(hFile); @@ -3119,7 +3827,7 @@ simplecpp::FileDataCache simplecpp::load(const simplecpp::TokenList &rawtokens, std::list filelist; // -include files - for (std::list::const_iterator it = dui.includes.begin(); it != dui.includes.end(); ++it) { + for (auto it = dui.includes.cbegin(); it != dui.includes.cend(); ++it) { const std::string &filename = *it; const auto loadResult = cache.get("", filename, dui, false, filenames, outputList); @@ -3128,11 +3836,12 @@ simplecpp::FileDataCache simplecpp::load(const simplecpp::TokenList &rawtokens, if (filedata == nullptr) { if (outputList) { - simplecpp::Output err(filenames); - err.type = simplecpp::Output::EXPLICIT_INCLUDE_NOT_FOUND; - err.location = Location(filenames); - err.msg = "Can not open include file '" + filename + "' that is explicitly included."; - outputList->push_back(err); + simplecpp::Output err{ + simplecpp::Output::EXPLICIT_INCLUDE_NOT_FOUND, + {}, + "Can not open include file '" + filename + "' that is explicitly included." + }; + outputList->emplace_back(std::move(err)); } continue; } @@ -3146,7 +3855,7 @@ simplecpp::FileDataCache simplecpp::load(const simplecpp::TokenList &rawtokens, if (dui.removeComments) filedata->tokens.removeComments(); - filelist.push_back(filedata->tokens.front()); + filelist.emplace_back(filedata->tokens.front()); } for (const Token *rawtok = rawtokens.cfront(); rawtok || !filelist.empty(); rawtok = rawtok ? rawtok->next : nullptr) { @@ -3162,7 +3871,7 @@ simplecpp::FileDataCache simplecpp::load(const simplecpp::TokenList &rawtokens, if (!rawtok || rawtok->str() != INCLUDE) continue; - const std::string &sourcefile = rawtok->location.file(); + const std::string &sourcefile = rawtokens.file(rawtok->location); const Token * const htok = rawtok->nextSkipComments(); if (!sameline(rawtok, htok)) @@ -3185,35 +3894,46 @@ simplecpp::FileDataCache simplecpp::load(const simplecpp::TokenList &rawtokens, if (dui.removeComments) filedata->tokens.removeComments(); - filelist.push_back(filedata->tokens.front()); + filelist.emplace_back(filedata->tokens.front()); } return cache; } -static bool preprocessToken(simplecpp::TokenList &output, const simplecpp::Token **tok1, simplecpp::MacroMap ¯os, std::vector &files, simplecpp::OutputList *outputList) +static bool preprocessToken(simplecpp::TokenList &output, const simplecpp::Token *&tok1, simplecpp::MacroMap ¯os, std::vector &files, simplecpp::OutputList *outputList, const simplecpp::DUI &dui) { - const simplecpp::Token * const tok = *tok1; - const simplecpp::MacroMap::const_iterator it = macros.find(tok->str()); + const simplecpp::Token * const tok = tok1; + const simplecpp::MacroMap::const_iterator it = tok->name ? macros.find(tok->str()) : macros.end(); if (it != macros.end()) { simplecpp::TokenList value(files); try { - *tok1 = it->second.expand(&value, tok, macros, files); - } catch (simplecpp::Macro::Error &err) { + tok1 = it->second.expand(value, tok, macros, files, outputList); + } catch (const simplecpp::Macro::invalidDirectiveAsMacroParameter &err) { if (outputList) { - simplecpp::Output out(files); - out.type = simplecpp::Output::SYNTAX_ERROR; - out.location = err.location; - out.msg = "failed to expand \'" + tok->str() + "\', " + err.what; - outputList->push_back(out); + simplecpp::Output out{ + simplecpp::Output::DIRECTIVE_AS_MACRO_PARAMETER, + err.location, + "failed to expand \'" + tok->str() + "\', " + err.what + }; + outputList->emplace_back(std::move(out)); + } + return false; + } catch (const simplecpp::Macro::Error &err) { + if (outputList) { + simplecpp::Output out{ + simplecpp::Output::SYNTAX_ERROR, + err.location, + "failed to expand \'" + tok->str() + "\', " + err.what + }; + outputList->emplace_back(std::move(out)); } return false; } output.takeTokens(value); } else { - if (!tok->comment) + if (!tok->comment || !dui.removeComments) output.push_back(new simplecpp::Token(*tok)); - *tok1 = tok->next; + tok1 = tok->next; } return true; } @@ -3254,20 +3974,20 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL std::map sizeOfType(rawtokens.sizeOfType); sizeOfType.insert(std::make_pair("char", sizeof(char))); sizeOfType.insert(std::make_pair("short", sizeof(short))); - sizeOfType.insert(std::make_pair("short int", sizeOfType["short"])); + sizeOfType.insert(std::make_pair("short int", sizeof(short))); sizeOfType.insert(std::make_pair("int", sizeof(int))); sizeOfType.insert(std::make_pair("long", sizeof(long))); - sizeOfType.insert(std::make_pair("long int", sizeOfType["long"])); + sizeOfType.insert(std::make_pair("long int", sizeof(long))); sizeOfType.insert(std::make_pair("long long", sizeof(long long))); sizeOfType.insert(std::make_pair("float", sizeof(float))); sizeOfType.insert(std::make_pair("double", sizeof(double))); sizeOfType.insert(std::make_pair("long double", sizeof(long double))); sizeOfType.insert(std::make_pair("char *", sizeof(char *))); sizeOfType.insert(std::make_pair("short *", sizeof(short *))); - sizeOfType.insert(std::make_pair("short int *", sizeOfType["short *"])); + sizeOfType.insert(std::make_pair("short int *", sizeof(short *))); sizeOfType.insert(std::make_pair("int *", sizeof(int *))); sizeOfType.insert(std::make_pair("long *", sizeof(long *))); - sizeOfType.insert(std::make_pair("long int *", sizeOfType["long *"])); + sizeOfType.insert(std::make_pair("long int *", sizeof(long *))); sizeOfType.insert(std::make_pair("long long *", sizeof(long long *))); sizeOfType.insert(std::make_pair("float *", sizeof(float *))); sizeOfType.insert(std::make_pair("double *", sizeof(double *))); @@ -3279,7 +3999,7 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL const bool hasInclude = isCpp17OrLater(dui) || isGnu(dui); MacroMap macros; bool strictAnsiDefined = false; - for (std::list::const_iterator it = dui.defines.begin(); it != dui.defines.end(); ++it) { + for (auto it = dui.defines.cbegin(); it != dui.defines.cend(); ++it) { const std::string ¯ostr = *it; const std::string::size_type eq = macrostr.find('='); const std::string::size_type par = macrostr.find('('); @@ -3290,8 +4010,32 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL continue; const std::string lhs(macrostr.substr(0,eq)); const std::string rhs(eq==std::string::npos ? std::string("1") : macrostr.substr(eq+1)); - const Macro macro(lhs, rhs, dummy); - macros.insert(std::pair(macro.name(), macro)); + try { + const Macro macro(lhs, rhs, dummy); + macros.insert(std::pair(macro.name(), macro)); + } catch (const std::runtime_error& e) { + if (outputList) { + simplecpp::Output err{ + Output::DUI_ERROR, + {}, + e.what() + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } catch (const simplecpp::Macro::Error& e) { + if (outputList) { + simplecpp::Output err{ + Output::DUI_ERROR, + {}, + e.what + }; + outputList->emplace_back(std::move(err)); + } + output.clear(); + return; + } } const bool strictAnsiUndefined = dui.undefined.find("__STRICT_ANSI__") != dui.undefined.cend(); @@ -3301,7 +4045,7 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL macros.insert(std::make_pair("__FILE__", Macro("__FILE__", "__FILE__", dummy))); macros.insert(std::make_pair("__LINE__", Macro("__LINE__", "__LINE__", dummy))); macros.insert(std::make_pair("__COUNTER__", Macro("__COUNTER__", "__COUNTER__", dummy))); - struct tm ltime = {}; + struct tm ltime {}; getLocaltime(ltime); macros.insert(std::make_pair("__DATE__", Macro("__DATE__", getDateDefine(<ime), dummy))); macros.insert(std::make_pair("__TIME__", Macro("__TIME__", getTimeDefine(<ime), dummy))); @@ -3316,10 +4060,12 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL const cppstd_t cpp_std = simplecpp::getCppStd(dui.std); if (cpp_std == CPPUnknown) { if (outputList) { - simplecpp::Output err(files); - err.type = Output::DUI_ERROR; - err.msg = "unknown standard specified: '" + dui.std + "'"; - outputList->push_back(err); + simplecpp::Output err{ + Output::DUI_ERROR, + {}, + "unknown standard specified: '" + dui.std + "'" + }; + outputList->emplace_back(std::move(err)); } output.clear(); return; @@ -3333,7 +4079,7 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL // True => code in current #if block should be kept // ElseIsTrue => code in current #if block should be dropped. the code in the #else should be kept. // AlwaysFalse => drop all code in #if and #else - enum IfState { True, ElseIsTrue, AlwaysFalse }; + enum IfState : std::uint8_t { True, ElseIsTrue, AlwaysFalse }; std::stack ifstates; std::stack iftokens; ifstates.push(True); @@ -3343,13 +4089,24 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL std::set pragmaOnce; includetokenstack.push(rawtokens.cfront()); - for (std::list::const_iterator it = dui.includes.begin(); it != dui.includes.end(); ++it) { + for (auto it = dui.includes.cbegin(); it != dui.includes.cend(); ++it) { const FileData *const filedata = cache.get("", *it, dui, false, files, outputList).first; - if (filedata != nullptr && filedata->tokens.cfront() != nullptr) + if (filedata == nullptr) { + if (outputList) { + simplecpp::Output err{ + simplecpp::Output::EXPLICIT_INCLUDE_NOT_FOUND, + {}, + "Can not open include file '" + *it + "' that is explicitly included." + }; + outputList->emplace_back(std::move(err)); + } + } + else if (filedata->tokens.cfront() != nullptr) { includetokenstack.push(filedata->tokens.cfront()); + } } - std::map > maybeUsedMacros; + std::map> maybeUsedMacros; for (const Token *rawtok = nullptr; rawtok || !includetokenstack.empty();) { if (rawtok == nullptr) { @@ -3369,13 +4126,16 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL continue; } - if (ifstates.size() <= 1U && (rawtok->str() == ELIF || rawtok->str() == ELSE || rawtok->str() == ENDIF)) { + if (ifstates.size() <= 1U && (rawtok->str() == ELIF || rawtok->str() == ELIFDEF || + rawtok->str() == ELIFNDEF || rawtok->str() == ELSE || + rawtok->str() == ENDIF)) { if (outputList) { - simplecpp::Output err(files); - err.type = Output::SYNTAX_ERROR; - err.location = rawtok->location; - err.msg = "#" + rawtok->str() + " without #if"; - outputList->push_back(err); + simplecpp::Output err{ + Output::SYNTAX_ERROR, + rawtok->location, + "#" + rawtok->str() + " without #if" + }; + outputList->emplace_back(std::move(err)); } output.clear(); return; @@ -3383,16 +4143,20 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL if (ifstates.top() == True && (rawtok->str() == ERROR || rawtok->str() == WARNING)) { if (outputList) { - simplecpp::Output err(rawtok->location.files); - err.type = rawtok->str() == ERROR ? Output::ERROR : Output::WARNING; - err.location = rawtok->location; + std::string msg; for (const Token *tok = rawtok->next; tok && sameline(rawtok,tok); tok = tok->next) { - if (!err.msg.empty() && isNameChar(tok->str()[0])) - err.msg += ' '; - err.msg += tok->str(); + if (!msg.empty() && isNameChar(tok->str()[0])) + msg += ' '; + msg += tok->str(); } - err.msg = '#' + rawtok->str() + ' ' + err.msg; - outputList->push_back(err); + msg = '#' + rawtok->str() + ' ' + msg; + simplecpp::Output err{ + rawtok->str() == ERROR ? Output::ERROR : Output::WARNING, + rawtok->location, + std::move(msg) + }; + + outputList->emplace_back(std::move(err)); } if (rawtok->str() == ERROR) { output.clear(); @@ -3412,23 +4176,25 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL else it->second = macro; } - } catch (const std::runtime_error &) { + } catch (const std::runtime_error &err) { if (outputList) { - simplecpp::Output err(files); - err.type = Output::SYNTAX_ERROR; - err.location = rawtok->location; - err.msg = "Failed to parse #define"; - outputList->push_back(err); + simplecpp::Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + std::string("Failed to parse #define, ") + err.what() + }; + outputList->emplace_back(std::move(out)); } output.clear(); return; - } catch (simplecpp::Macro::Error &err) { + } catch (const simplecpp::Macro::Error &err) { if (outputList) { - simplecpp::Output out(files); - out.type = simplecpp::Output::SYNTAX_ERROR; - out.location = err.location; - out.msg = "Failed to parse #define, " + err.what; - outputList->push_back(out); + simplecpp::Output out{ + simplecpp::Output::SYNTAX_ERROR, + err.location, + "Failed to parse #define, " + err.what + }; + outputList->emplace_back(std::move(out)); } output.clear(); return; @@ -3442,7 +4208,7 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL TokenList inc2(files); if (!inc1.empty() && inc1.cfront()->name) { const Token *inctok = inc1.cfront(); - if (!preprocessToken(inc2, &inctok, macros, files, outputList)) { + if (!preprocessToken(inc2, inctok, macros, files, outputList, dui)) { output.clear(); return; } @@ -3464,11 +4230,12 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL if (inc2.empty() || inc2.cfront()->str().size() <= 2U) { if (outputList) { - simplecpp::Output err(files); - err.type = Output::SYNTAX_ERROR; - err.location = rawtok->location; - err.msg = "No header in #include"; - outputList->push_back(err); + simplecpp::Output err{ + Output::SYNTAX_ERROR, + rawtok->location, + "No header in #include" + }; + outputList->emplace_back(std::move(err)); } output.clear(); return; @@ -3478,50 +4245,71 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL const bool systemheader = (inctok->str()[0] == '<'); const std::string header(inctok->str().substr(1U, inctok->str().size() - 2U)); - const FileData *const filedata = cache.get(rawtok->location.file(), header, dui, systemheader, files, outputList).first; + const FileData *const filedata = cache.get(rawtokens.file(rawtok->location), header, dui, systemheader, files, outputList).first; if (filedata == nullptr) { if (outputList) { - simplecpp::Output out(files); - out.type = Output::MISSING_HEADER; - out.location = rawtok->location; - out.msg = "Header not found: " + inctok->str(); - outputList->push_back(out); + simplecpp::Output out{ + simplecpp::Output::MISSING_HEADER, + rawtok->location, + "Header not found: " + inctok->str() + }; + outputList->emplace_back(std::move(out)); } } else if (includetokenstack.size() >= 400) { if (outputList) { - simplecpp::Output out(files); - out.type = Output::INCLUDE_NESTED_TOO_DEEPLY; - out.location = rawtok->location; - out.msg = "#include nested too deeply"; - outputList->push_back(out); + simplecpp::Output out{ + simplecpp::Output::INCLUDE_NESTED_TOO_DEEPLY, + rawtok->location, + "#include nested too deeply" + }; + outputList->emplace_back(std::move(out)); } } else if (pragmaOnce.find(filedata->filename) == pragmaOnce.end()) { includetokenstack.push(gotoNextLine(rawtok)); rawtok = filedata->tokens.cfront(); continue; } - } else if (rawtok->str() == IF || rawtok->str() == IFDEF || rawtok->str() == IFNDEF || rawtok->str() == ELIF) { + } else if (rawtok->str() == IF || rawtok->str() == IFDEF || rawtok->str() == IFNDEF || rawtok->str() == ELIF || + rawtok->str() == ELIFDEF || rawtok->str() == ELIFNDEF) { if (!sameline(rawtok,rawtok->next)) { if (outputList) { - simplecpp::Output out(files); - out.type = Output::SYNTAX_ERROR; - out.location = rawtok->location; - out.msg = "Syntax error in #" + rawtok->str(); - outputList->push_back(out); + simplecpp::Output out{ + simplecpp::Output::SYNTAX_ERROR, + rawtok->location, + "Syntax error in #" + rawtok->str() + }; + outputList->emplace_back(std::move(out)); } output.clear(); return; } + const bool isElif = (rawtok->str() == ELIF || rawtok->str() == ELIFDEF || rawtok->str() == ELIFNDEF); + bool conditionIsTrue; - if (ifstates.top() == AlwaysFalse || (ifstates.top() == ElseIsTrue && rawtok->str() != ELIF)) + // An #if is only evaluated in an active group. An #elif is only evaluated + // if no previous group was taken; otherwise it is skipped (C 6.10.1p6). + if (isElif ? (ifstates.top() != ElseIsTrue) : (ifstates.top() != True)) { conditionIsTrue = false; - else if (rawtok->str() == IFDEF) { - conditionIsTrue = (macros.find(rawtok->next->str()) != macros.end() || (hasInclude && rawtok->next->str() == HAS_INCLUDE)); - maybeUsedMacros[rawtok->next->str()].push_back(rawtok->next->location); - } else if (rawtok->str() == IFNDEF) { - conditionIsTrue = (macros.find(rawtok->next->str()) == macros.end() && !(hasInclude && rawtok->next->str() == HAS_INCLUDE)); - maybeUsedMacros[rawtok->next->str()].push_back(rawtok->next->location); + } + else if (rawtok->str() == IFDEF || rawtok->str() == ELIFDEF) { + const std::string &name = rawtok->next->str(); + conditionIsTrue = (macros.find(name) != macros.end() || (hasInclude && name == HAS_INCLUDE)); + maybeUsedMacros[name].emplace_back(rawtok->next->location); + if (ifCond) { + const std::string E = conditionIsTrue ? "1" : "0"; + const long long result = conditionIsTrue ? 1 : 0; + ifCond->emplace_back(rawtok->location, E, result); + } + } else if (rawtok->str() == IFNDEF || rawtok->str() == ELIFNDEF) { + const std::string &name = rawtok->next->str(); + conditionIsTrue = (macros.find(name) == macros.end() && !(hasInclude && name == HAS_INCLUDE)); + maybeUsedMacros[name].emplace_back(rawtok->next->location); + if (ifCond) { + const std::string E = conditionIsTrue ? "1" : "0"; + const long long result = conditionIsTrue ? 1 : 0; + ifCond->emplace_back(rawtok->location, E, result); + } } else { /*if (rawtok->str() == IF || rawtok->str() == ELIF)*/ TokenList expr(files); for (const Token *tok = rawtok->next; tok && tok->location.sameline(rawtok->location); tok = tok->next) { @@ -3535,24 +4323,26 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL const bool par = (tok && tok->op == '('); if (par) tok = tok->next; - maybeUsedMacros[rawtok->next->str()].push_back(rawtok->next->location); if (tok) { - if (macros.find(tok->str()) != macros.end()) + if (macros.find(tok->str()) != macros.end()) { + maybeUsedMacros[tok->str()].emplace_back(tok->location); expr.push_back(new Token("1", tok->location)); - else if (hasInclude && tok->str() == HAS_INCLUDE) + } else if (hasInclude && tok->str() == HAS_INCLUDE) { expr.push_back(new Token("1", tok->location)); - else + } else { expr.push_back(new Token("0", tok->location)); + } } if (par) tok = tok ? tok->next : nullptr; if (!tok || !sameline(rawtok,tok) || (par && tok->op != ')')) { if (outputList) { - Output out(rawtok->location.files); - out.type = Output::SYNTAX_ERROR; - out.location = rawtok->location; - out.msg = "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition"; - outputList->push_back(out); + Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition" + }; + outputList->emplace_back(std::move(out)); } output.clear(); return; @@ -3567,8 +4357,8 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL tok = tok->next; bool closingAngularBracket = false; if (tok) { - const std::string &sourcefile = rawtok->location.file(); - const bool systemheader = (tok && tok->op == '<'); + const std::string &sourcefile = rawtokens.file(rawtok->location); + const bool systemheader = tok->op == '<'; std::string header; if (systemheader) { @@ -3580,19 +4370,22 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL header = tok->str().substr(1U, tok->str().size() - 2U); closingAngularBracket = true; } - std::ifstream f; - const std::string header2 = openHeader(f,dui,sourcefile,header,systemheader); - expr.push_back(new Token(header2.empty() ? "0" : "1", tok->location)); + if (tok) { + std::ifstream f; + const std::string header2 = openHeader(f,dui,sourcefile,header,systemheader); + expr.push_back(new Token(header2.empty() ? "0" : "1", tok->location)); + } } if (par) tok = tok ? tok->next : nullptr; if (!tok || !sameline(rawtok,tok) || (par && tok->op != ')') || (!closingAngularBracket)) { if (outputList) { - Output out(rawtok->location.files); - out.type = Output::SYNTAX_ERROR; - out.location = rawtok->location; - out.msg = "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition"; - outputList->push_back(out); + Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition" + }; + outputList->emplace_back(std::move(out)); } output.clear(); return; @@ -3600,10 +4393,12 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL continue; } - maybeUsedMacros[rawtok->next->str()].push_back(rawtok->next->location); + // usage of defined macros is recorded when they are expanded + if (macros.find(tok->str()) == macros.end()) + maybeUsedMacros[tok->str()].emplace_back(tok->location); const Token *tmp = tok; - if (!preprocessToken(expr, &tmp, macros, files, outputList)) { + if (!preprocessToken(expr, tmp, macros, files, outputList, dui)) { output.clear(); return; } @@ -3618,39 +4413,40 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL E += (E.empty() ? "" : " ") + tok->str(); const long long result = evaluate(expr, dui, sizeOfType); conditionIsTrue = (result != 0); - ifCond->push_back(IfCond(rawtok->location, E, result)); + ifCond->emplace_back(rawtok->location, E, result); } else { const long long result = evaluate(expr, dui, sizeOfType); conditionIsTrue = (result != 0); } - } catch (const std::exception &e) { + } catch (const std::runtime_error &e) { if (outputList) { - Output out(rawtok->location.files); - out.type = Output::SYNTAX_ERROR; - out.location = rawtok->location; - out.msg = "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition"; + std::string msg = "failed to evaluate " + std::string(rawtok->str() == IF ? "#if" : "#elif") + " condition"; if (e.what() && *e.what()) - out.msg += std::string(", ") + e.what(); - outputList->push_back(out); + msg += std::string(", ") + e.what(); + Output out{ + Output::SYNTAX_ERROR, + rawtok->location, + std::move(msg) + }; + outputList->emplace_back(std::move(out)); } output.clear(); return; } } - if (rawtok->str() != ELIF) { + if (rawtok->str() != ELIF && rawtok->str() != ELIFDEF && rawtok->str() != ELIFNDEF) { // push a new ifstate.. if (ifstates.top() != True) ifstates.push(AlwaysFalse); else ifstates.push(conditionIsTrue ? True : ElseIsTrue); iftokens.push(rawtok); - } else if (ifstates.top() == True) { - ifstates.top() = AlwaysFalse; - iftokens.top()->nextcond = rawtok; - iftokens.top() = rawtok; - } else if (ifstates.top() == ElseIsTrue && conditionIsTrue) { - ifstates.top() = True; + } else { + if (ifstates.top() == True) + ifstates.top() = AlwaysFalse; + else if (ifstates.top() == ElseIsTrue && conditionIsTrue) + ifstates.top() = True; iftokens.top()->nextcond = rawtok; iftokens.top() = rawtok; } @@ -3671,7 +4467,7 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL macros.erase(tok->str()); } } else if (ifstates.top() == True && rawtok->str() == PRAGMA && rawtok->next && rawtok->next->str() == ONCE && sameline(rawtok,rawtok->next)) { - pragmaOnce.insert(rawtok->location.file()); + pragmaOnce.insert(rawtokens.file(rawtok->location)); } if (ifstates.top() != True && rawtok->nextcond) rawtok = rawtok->nextcond->previous; @@ -3700,7 +4496,7 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL const Location loc(rawtok->location); TokenList tokens(files); - if (!preprocessToken(tokens, &rawtok, macros, files, outputList)) { + if (!preprocessToken(tokens, rawtok, macros, files, outputList, dui)) { output.clear(); return; } @@ -3727,11 +4523,11 @@ void simplecpp::preprocess(simplecpp::TokenList &output, const simplecpp::TokenL const std::list& temp = maybeUsedMacros[macro.name()]; usage.insert(usage.end(), temp.begin(), temp.end()); for (std::list::const_iterator usageIt = usage.begin(); usageIt != usage.end(); ++usageIt) { - MacroUsage mu(usageIt->files, macro.valueDefinedInCode()); + MacroUsage mu(macro.valueDefinedInCode()); mu.macroName = macro.name(); mu.macroLocation = macro.defineLocation(); mu.useLocation = *usageIt; - macroUsage->push_back(mu); + macroUsage->emplace_back(std::move(mu)); } } } @@ -3746,17 +4542,33 @@ simplecpp::cstd_t simplecpp::getCStd(const std::string &std) { if (std == "c90" || std == "c89" || std == "iso9899:1990" || std == "iso9899:199409" || std == "gnu90" || std == "gnu89") return C89; - if (std == "c99" || std == "c9x" || std == "iso9899:1999" || std == "iso9899:199x" || std == "gnu99"|| std == "gnu9x") + if (std == "c99" || std == "c9x" || std == "iso9899:1999" || std == "iso9899:199x" || std == "gnu99" || std == "gnu9x") return C99; if (std == "c11" || std == "c1x" || std == "iso9899:2011" || std == "gnu11" || std == "gnu1x") return C11; - if (std == "c17" || std == "c18" || std == "iso9899:2017" || std == "iso9899:2018" || std == "gnu17"|| std == "gnu18") + if (std == "c17" || std == "c18" || std == "iso9899:2017" || std == "iso9899:2018" || std == "gnu17" || std == "gnu18") return C17; if (std == "c23" || std == "gnu23" || std == "c2x" || std == "gnu2x") return C23; + if (std == "c2y" || std == "gnu2y") + return C2Y; return CUnknown; } +const char *simplecpp::getCStdName(cstd_t std) +{ + switch (std) { + case CUnknown: return "C"; + case C89: return "C89"; + case C99: return "C99"; + case C11: return "C11"; + case C17: return "C17"; + case C23: return "C23"; + case C2Y: return "C2Y"; + } + return ""; +} + std::string simplecpp::getCStdString(cstd_t std) { switch (std) { @@ -3775,6 +4587,10 @@ std::string simplecpp::getCStdString(cstd_t std) // Clang 14, 15, 16, 17 return "202000L" // Clang 9, 10, 11, 12, 13, 14, 15, 16, 17 do not support "c23" and "gnu23" return "202311L"; + case C2Y: + // supported by GCC 15+ and Clang 19+ + // Clang 19, 20, 21, 22 return "202400L" + return "202500L"; case CUnknown: return ""; } @@ -3805,6 +4621,21 @@ simplecpp::cppstd_t simplecpp::getCppStd(const std::string &std) return CPPUnknown; } +const char *simplecpp::getCppStdName(cppstd_t std) +{ + switch (std) { + case CPPUnknown: return "C++"; + case CPP03: return "C++03"; + case CPP11: return "C++11"; + case CPP14: return "C++14"; + case CPP17: return "C++17"; + case CPP20: return "C++20"; + case CPP23: return "C++23"; + case CPP26: return "C++26"; + } + return ""; +} + std::string simplecpp::getCppStdString(cppstd_t std) { switch (std) { @@ -3826,7 +4657,7 @@ std::string simplecpp::getCppStdString(cppstd_t std) // Clang 17, 18 return "202302L" return "202302L"; case CPP26: - // supported by Clang 17+ + // supported by GCC 14+ and Clang 17+ return "202400L"; case CPPUnknown: return ""; diff --git a/simplecpp.h b/simplecpp.h index 8268fa8d..2b560d27 100644 --- a/simplecpp.h +++ b/simplecpp.h @@ -6,12 +6,10 @@ #ifndef simplecppH #define simplecppH -#if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) -# define SIMPLECPP_WINDOWS -#endif - #include +#include #include +#include #include #include #include @@ -19,7 +17,18 @@ #include #include #include +#include #include +#if __cplusplus >= 202002L +# include +#endif + +#if defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) +#include +#endif +#ifdef __cpp_lib_span +#include +#endif #ifdef _WIN32 # ifdef SIMPLECPP_EXPORT @@ -33,12 +42,6 @@ # define SIMPLECPP_LIB #endif -#ifdef SIMPLECPP_WINDOWS -# include -#else -# include -#endif - #if defined(_MSC_VER) # pragma warning(push) // suppress warnings about "conversion from 'type1' to 'type2', possible loss of data" @@ -46,34 +49,89 @@ # pragma warning(disable : 4244) #endif +// provide legacy (i.e. raw pointer) API for TokenList +// note: std::istream has an overhead compared to raw pointers +#ifndef SIMPLECPP_TOKENLIST_ALLOW_PTR +// still provide the legacy API in case we lack the performant wrappers +# if !defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) +# define SIMPLECPP_TOKENLIST_ALLOW_PTR 1 +# else +# define SIMPLECPP_TOKENLIST_ALLOW_PTR 0 +# endif +#endif + +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ <= 9 +// Hack to workaround GCC bug. +// Details: https://trac.cppcheck.net/ticket/14850 +// seen on g++ before 10.x +#define SIMPLECPP_NOEXCEPT +#else +#define SIMPLECPP_NOEXCEPT noexcept +#endif + namespace simplecpp { /** C code standard */ - enum cstd_t { CUnknown=-1, C89, C99, C11, C17, C23 }; + enum cstd_t : std::int8_t { CUnknown=-1, C89, C99, C11, C17, C23, C2Y }; /** C++ code standard */ - enum cppstd_t { CPPUnknown=-1, CPP03, CPP11, CPP14, CPP17, CPP20, CPP23, CPP26 }; + enum cppstd_t : std::int8_t { CPPUnknown=-1, CPP03, CPP11, CPP14, CPP17, CPP20, CPP23, CPP26 }; + + using TokenString = std::string; + +#if defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) + using View = std::string_view; +#else + struct View + { + // cppcheck-suppress noExplicitConstructor + View(const char* data) + : mData(data) + , mSize(strlen(data)) + {} + + // only provide when std::span is not available so using untyped initialization won't use View +#if !defined(__cpp_lib_span) + View(const char* data, std::size_t size) + : mData(data) + , mSize(size) + {} + + // cppcheck-suppress noExplicitConstructor + View(const std::string& str) + : mData(str.data()) + , mSize(str.size()) + {} +#endif // !defined(__cpp_lib_span) + + const char* data() const { + return mData; + } + + std::size_t size() const { + return mSize; + } + + private: + const char* mData; + std::size_t mSize; + }; +#endif // defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) - typedef std::string TokenString; class Macro; - class FileDataCache; /** * Location in source code */ - class SIMPLECPP_LIB Location { - public: - explicit Location(const std::vector &f) : files(f), fileIndex(0), line(1U), col(0U) {} + struct SIMPLECPP_LIB Location { + Location() = default; + Location(unsigned int fileIndex, unsigned int line, unsigned int col) + : fileIndex(fileIndex) + , line(line) + , col(col) + {} - Location(const Location &loc) : files(loc.files), fileIndex(loc.fileIndex), line(loc.line), col(loc.col) {} - - Location &operator=(const Location &other) { - if (this != &other) { - fileIndex = other.fileIndex; - line = other.line; - col = other.col; - } - return *this; - } + Location(const Location &loc) = default; + Location &operator=(const Location &other) = default; /** increment this location by string */ void adjust(const std::string &str); @@ -90,16 +148,9 @@ namespace simplecpp { return fileIndex == other.fileIndex && line == other.line; } - const std::string& file() const { - return fileIndex < files.size() ? files[fileIndex] : emptyFileName; - } - - const std::vector &files; - unsigned int fileIndex; - unsigned int line; - unsigned int col; - private: - static const std::string emptyFileName; + unsigned int fileIndex{}; + unsigned int line{}; + unsigned int col{}; }; /** @@ -109,21 +160,14 @@ namespace simplecpp { class SIMPLECPP_LIB Token { public: Token(const TokenString &s, const Location &loc, bool wsahead = false) : - whitespaceahead(wsahead), location(loc), previous(nullptr), next(nullptr), nextcond(nullptr), string(s) { + whitespaceahead(wsahead), location(loc), string(s) { flags(); } Token(const Token &tok) : - macro(tok.macro), op(tok.op), comment(tok.comment), name(tok.name), number(tok.number), whitespaceahead(tok.whitespaceahead), location(tok.location), previous(nullptr), next(nullptr), nextcond(nullptr), string(tok.string), mExpandedFrom(tok.mExpandedFrom) { - } + macro(tok.macro), op(tok.op), comment(tok.comment), name(tok.name), number(tok.number), whitespaceahead(tok.whitespaceahead), location(tok.location), string(tok.string), mExpandedFrom(tok.mExpandedFrom) {} - void flags() { - name = (std::isalpha(static_cast(string[0])) || string[0] == '_' || string[0] == '$') - && (std::memchr(string.c_str(), '\'', string.size()) == nullptr); - comment = string.size() > 1U && string[0] == '/' && (string[1] == '/' || string[1] == '*'); - number = isNumberLike(string); - op = (string.size() == 1U && !name && !comment && !number) ? string[0] : '\0'; - } + Token &operator=(const Token &tok) = delete; const TokenString& str() const { return string; @@ -136,9 +180,9 @@ namespace simplecpp { bool isOneOf(const char ops[]) const; bool startsWithOneOf(const char c[]) const; bool endsWithOneOf(const char c[]) const; - static bool isNumberLike(const std::string& str) { - return std::isdigit(static_cast(str[0])) || - (str.size() > 1U && (str[0] == '-' || str[0] == '+') && std::isdigit(static_cast(str[1]))); + static bool isNumberLike(const std::string& s) { + return std::isdigit(static_cast(s[0])) || + (s.size() > 1U && (s[0] == '-' || s[0] == '+') && std::isdigit(static_cast(s[1]))); } TokenString macro; @@ -148,9 +192,9 @@ namespace simplecpp { bool number; bool whitespaceahead; Location location; - Token *previous; - Token *next; - mutable const Token *nextcond; + Token *previous{}; + Token *next{}; + mutable const Token *nextcond{}; const Token *previousSkipComments() const { const Token *tok = this->previous; @@ -175,39 +219,65 @@ namespace simplecpp { bool isExpandedFrom(const Macro* m) const { return mExpandedFrom.find(m) != mExpandedFrom.end(); } + void markExpandedFrom(const Macro* m) { + mExpandedFrom.insert(m); + } void printAll() const; void printOut() const; private: + void flags() { + name = (std::isalpha(static_cast(string[0])) || string[0] == '_' || string[0] == '$') + && (std::memchr(string.c_str(), '\'', string.size()) == nullptr); + comment = string.size() > 1U && string[0] == '/' && (string[1] == '/' || string[1] == '*'); + number = isNumberLike(string); + op = (string.size() == 1U && !name && !comment && !number) ? string[0] : '\0'; + } + TokenString string; std::set mExpandedFrom; - - // Not implemented - prevent assignment - Token &operator=(const Token &tok); }; /** Output from preprocessor */ struct SIMPLECPP_LIB Output { - explicit Output(const std::vector &files) : type(ERROR), location(files) {} - enum Type { + enum Type : std::uint8_t { ERROR, /* #error */ WARNING, /* #warning */ MISSING_HEADER, INCLUDE_NESTED_TOO_DEEPLY, SYNTAX_ERROR, + DIRECTIVE_AS_MACRO_PARAMETER, PORTABILITY_BACKSLASH, + PORTABILITY_LINE_DIRECTIVE, + PORTABILITY_NO_EOF_NEWLINE, + PORTABILITY_STRINGIFY, UNHANDLED_CHAR_ERROR, EXPLICIT_INCLUDE_NOT_FOUND, FILE_NOT_FOUND, DUI_ERROR } type; - explicit Output(const std::vector& files, Type type, const std::string& msg) : type(type), location(files), msg(msg) {} + Output(Type type, const Location& loc, std::string msg) : type(type), location(loc), msg(std::move(msg)) {} Location location; std::string msg; }; - typedef std::list OutputList; + using OutputList = std::list; + + /** + * Command line preprocessor settings. + * On the command line these are configured by -D, -U, -I, --include, -std + */ + struct SIMPLECPP_LIB DUI { + DUI() = default; + std::list defines; + std::set undefined; + std::list includePaths; + std::list includes; + std::string std; + bool clearIncludeCache{}; + bool removeComments{}; /** remove comment tokens from included files */ + }; /** List of tokens. */ class SIMPLECPP_LIB TokenList { @@ -216,13 +286,45 @@ namespace simplecpp { explicit TokenList(std::vector &filenames); /** generates a token list from the given std::istream parameter */ - TokenList(std::istream &istr, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr); + TokenList(std::istream &istr, std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr); + /** generates a token list from the given buffer */ + template + TokenList(const char (&data)[size], std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data), size-1, filenames, filename, dui, outputList, 0) + {} + /** generates a token list from the given buffer */ + template + TokenList(const unsigned char (&data)[size], std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(data, size-1, filenames, filename, dui, outputList, 0) + {} +#if SIMPLECPP_TOKENLIST_ALLOW_PTR /** generates a token list from the given buffer */ - TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr); + TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(data, size, filenames, filename, dui, outputList, 0) + {} /** generates a token list from the given buffer */ - TokenList(const char* data, std::size_t size, std::vector &filenames, const std::string &filename=std::string(), OutputList *outputList = nullptr); + TokenList(const char* data, std::size_t size, std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data), size, filenames, filename, dui, outputList, 0) + {} +#endif // SIMPLECPP_TOKENLIST_ALLOW_PTR + /** generates a token list from the given buffer */ + TokenList(View data, std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data.data()), data.size(), filenames, filename, dui, outputList, 0) + {} +#ifdef __cpp_lib_span + /** generates a token list from the given buffer */ + TokenList(std::span data, std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(reinterpret_cast(data.data()), data.size(), filenames, filename, dui, outputList, 0) + {} + + /** generates a token list from the given buffer */ + TokenList(std::span data, std::vector &filenames, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr) + : TokenList(data.data(), data.size(), filenames, filename, dui, outputList, 0) + {} +#endif // __cpp_lib_span + /** generates a token list from the given filename parameter */ - TokenList(const std::string &filename, std::vector &filenames, OutputList *outputList = nullptr); + TokenList(const std::string &filename, std::vector &filenames, const DUI &dui = {}, OutputList *outputList = nullptr); TokenList(const TokenList &other); TokenList(TokenList &&other); ~TokenList(); @@ -235,10 +337,14 @@ namespace simplecpp { } void push_back(Token *tok); - void dump() const; - std::string stringify() const; + void dump(bool linenrs = false) const; + std::string stringify(bool linenrs = false) const; - void readfile(Stream &stream, const std::string &filename=std::string(), OutputList *outputList = nullptr); + void readfile(Stream &stream, const std::string &filename=std::string(), const DUI &dui = {}, OutputList *outputList = nullptr); + /** + * @throws std::overflow_error thrown on overflow or division by zero + * @throws std::runtime_error thrown on invalid expressions + */ void constFold(); void removeComments(); @@ -295,24 +401,54 @@ namespace simplecpp { return files; } + const std::string& file(const Location& loc) const; + private: + TokenList(const unsigned char* data, std::size_t size, std::vector &filenames, const std::string &filename, const DUI &dui, OutputList *outputList, int /*unused*/); + void combineOperators(); void constFoldUnaryNotPosNeg(Token *tok); + /** + * @throws std::overflow_error thrown on overflow or division by zero + */ void constFoldMulDivRem(Token *tok); void constFoldAddSub(Token *tok); void constFoldShift(Token *tok); void constFoldComparison(Token *tok); void constFoldBitwise(Token *tok); void constFoldLogicalOp(Token *tok); - void constFoldQuestionOp(Token **tok1); + /** + * @throws std::runtime_error thrown on invalid expressions + */ + void constFoldQuestionOp(Token *&tok1); std::string readUntil(Stream &stream, const Location &location, char start, char end, OutputList *outputList); - void lineDirective(unsigned int fileIndex, unsigned int line, Location *location); + void lineDirective(unsigned int fileIndex_, unsigned int line, Location &location); - std::string lastLine(int maxsize=1000) const; const Token* lastLineTok(int maxsize=1000) const; - bool isLastLinePreprocessor(int maxsize=1000) const; + const Token* isLastLinePreprocessor(int maxsize=1000) const; + + /** + * Read a block comment, up to and including the closing star-slash. + * Each backslash-newline is removed from the text. If the comment is + * on a continued line or in a preprocessor directive, its newlines are + * removed too, so that the directive keeps going after the comment. + * @param stream the stream to read from. The first '/' is already + * read, and the next character is '*'. + * @param multiline incremented for each newline that is removed + * @return the comment text + */ + TokenString readBlockComment(Stream &stream, unsigned int &multiline); + /** + * Read the text of a #error or #warning directive, up to but not + * including the newline, and add it as one token. A backslash-newline + * continues the text on the next line and is removed from it. + * @param stream the stream to read from + * @param ch the first character of the text, already read from the stream + * @param location the location of the text. Updated to point after it. + */ + void readErrorText(Stream &stream, unsigned char ch, Location &location); unsigned int fileIndex(const std::string &filename); @@ -323,11 +459,11 @@ namespace simplecpp { /** Tracking how macros are used */ struct SIMPLECPP_LIB MacroUsage { - explicit MacroUsage(const std::vector &f, bool macroValueKnown_) : macroLocation(f), useLocation(f), macroValueKnown(macroValueKnown_) {} + explicit MacroUsage(bool macroValueKnown_) : macroValueKnown(macroValueKnown_) {} std::string macroName; - Location macroLocation; - Location useLocation; - bool macroValueKnown; + Location macroLocation; + Location useLocation; + bool macroValueKnown; }; /** Tracking #if/#elif expressions */ @@ -338,21 +474,6 @@ namespace simplecpp { long long result; // condition result }; - /** - * Command line preprocessor settings. - * On the command line these are configured by -D, -U, -I, --include, -std - */ - struct SIMPLECPP_LIB DUI { - DUI() : clearIncludeCache(false), removeComments(false) {} - std::list defines; - std::set undefined; - std::list includePaths; - std::list includes; - std::string std; - bool clearIncludeCache; - bool removeComments; /** remove comment tokens from included files */ - }; - struct SIMPLECPP_LIB FileData { /** The canonical filename associated with this data */ std::string filename; @@ -362,35 +483,33 @@ namespace simplecpp { class SIMPLECPP_LIB FileDataCache { public: - FileDataCache() = default; + FileDataCache(); + ~FileDataCache(); FileDataCache(const FileDataCache &) = delete; - FileDataCache(FileDataCache &&) = default; + FileDataCache(FileDataCache &&) SIMPLECPP_NOEXCEPT; FileDataCache &operator=(const FileDataCache &) = delete; - FileDataCache &operator=(FileDataCache &&) = default; + FileDataCache &operator=(FileDataCache &&) SIMPLECPP_NOEXCEPT; /** Get the cached data for a file, or load and then return it if it isn't cached. * returns the file data and true if the file was loaded, false if it was cached. */ std::pair get(const std::string &sourcefile, const std::string &header, const DUI &dui, bool systemheader, std::vector &filenames, OutputList *outputList); void insert(FileData data) { - FileData *const newdata = new FileData(std::move(data)); + // NOLINTNEXTLINE(misc-const-correctness) - FP + auto *const newdata = new FileData(std::move(data)); mData.emplace_back(newdata); mNameMap.emplace(newdata->filename, newdata); } - void clear() { - mNameMap.clear(); - mIdMap.clear(); - mData.clear(); - } + void clear(); - typedef std::vector> container_type; - typedef container_type::iterator iterator; - typedef container_type::const_iterator const_iterator; - typedef container_type::size_type size_type; + using container_type = std::vector>; + using iterator = container_type::iterator; + using const_iterator = container_type::const_iterator; + using size_type = container_type::size_type; size_type size() const { return mData.size(); @@ -414,54 +533,54 @@ namespace simplecpp { return mData.cend(); } - private: - struct FileID { -#ifdef SIMPLECPP_WINDOWS - struct { - std::uint64_t VolumeSerialNumber; - struct { - std::uint64_t IdentifierHi; - std::uint64_t IdentifierLo; - } FileId; - } fileIdInfo; - - bool operator==(const FileID &that) const noexcept { - return fileIdInfo.VolumeSerialNumber == that.fileIdInfo.VolumeSerialNumber && - fileIdInfo.FileId.IdentifierHi == that.fileIdInfo.FileId.IdentifierHi && - fileIdInfo.FileId.IdentifierLo == that.fileIdInfo.FileId.IdentifierLo; - } -#else - dev_t dev; - ino_t ino; + using load_callback_type = std::function; - bool operator==(const FileID& that) const noexcept { - return dev == that.dev && ino == that.ino; - } -#endif - struct Hasher { - std::size_t operator()(const FileID &id) const { -#ifdef SIMPLECPP_WINDOWS - return static_cast(id.fileIdInfo.FileId.IdentifierHi ^ id.fileIdInfo.FileId.IdentifierLo ^ - id.fileIdInfo.VolumeSerialNumber); -#else - return static_cast(id.dev) ^ static_cast(id.ino); -#endif - } - }; - }; + void set_load_callback(load_callback_type cb) { + mLoadCallback = std::move(cb); + } - using name_map_type = std::unordered_map; - using id_map_type = std::unordered_map; + private: + struct Impl; + std::unique_ptr mImpl; - static bool getFileId(const std::string &path, FileID &id); + using name_map_type = std::unordered_map; std::pair tryload(name_map_type::iterator &name_it, const DUI &dui, std::vector &filenames, OutputList *outputList); + std::pair get_private(const std::string &sourcefile, const std::string &header, const DUI &dui, bool systemheader, std::vector &filenames, OutputList *outputList); container_type mData; name_map_type mNameMap; - id_map_type mIdMap; + load_callback_type mLoadCallback; }; + /** Converts character literal (including prefix, but not ud-suffix) to long long value. + * + * Assumes ASCII-compatible single-byte encoded str for narrow literals + * and UTF-8 otherwise. + * + * For target assumes + * - execution character set encoding matching str + * - UTF-32 execution wide-character set encoding + * - requirements for __STDC_UTF_16__, __STDC_UTF_32__ and __STDC_ISO_10646__ satisfied + * - char16_t is 16bit wide + * - char32_t is 32bit wide + * - wchar_t is 32bit wide and unsigned + * - matching char signedness to host + * - matching sizeof(int) to host + * + * For host assumes + * - ASCII-compatible execution character set + * + * For host and target assumes + * - CHAR_BIT == 8 + * - two's complement + * + * Implements multi-character narrow literals according to GCC's behavior, + * except multi code unit universal character names are not supported. + * Multi-character wide literals are not supported. + * Limited support of universal character names for non-UTF-8 execution character set encodings. + * @throws std::runtime_error thrown on invalid literal + */ SIMPLECPP_LIB long long characterLiteralToLL(const std::string& str); SIMPLECPP_LIB FileDataCache load(const TokenList &rawtokens, std::vector &filenames, const DUI &dui, OutputList *outputList = nullptr, FileDataCache cache = {}); @@ -494,9 +613,15 @@ namespace simplecpp { /** Returns the C version a given standard */ SIMPLECPP_LIB cstd_t getCStd(const std::string &std); + /** Returns the name of a C standard */ + SIMPLECPP_LIB const char *getCStdName(cstd_t std); + /** Returns the C++ version a given standard */ SIMPLECPP_LIB cppstd_t getCppStd(const std::string &std); + /** Returns the name of a C++ standard */ + SIMPLECPP_LIB const char *getCppStdName(cppstd_t std); + /** Returns the __STDC_VERSION__ value for a given standard */ SIMPLECPP_LIB std::string getCStdString(const std::string &std); SIMPLECPP_LIB std::string getCStdString(cstd_t std); @@ -504,10 +629,17 @@ namespace simplecpp { /** Returns the __cplusplus value for a given standard */ SIMPLECPP_LIB std::string getCppStdString(const std::string &std); SIMPLECPP_LIB std::string getCppStdString(cppstd_t std); + + /** Checks if given path is absolute */ + SIMPLECPP_LIB bool isAbsolutePath(const std::string &path); } +#undef SIMPLECPP_TOKENLIST_ALLOW_PTR + #if defined(_MSC_VER) # pragma warning(pop) #endif +#undef SIMPLECPP_LIB + #endif diff --git a/test.cpp b/test.cpp index ccb653ca..3f996167 100644 --- a/test.cpp +++ b/test.cpp @@ -5,24 +5,48 @@ #include "simplecpp.h" +#include #include +#include #include #include +#include #include +#include #include +#include #include #include #include #include #include +#ifdef __cpp_lib_string_view +#include +#endif + +#ifndef SIMPLECPP_TEST_SOURCE_DIR +#error "SIMPLECPP_TEST_SOURCE_DIR is not defined." +#endif + #define STRINGIZE_(x) #x #define STRINGIZE(x) STRINGIZE_(x) +static const std::string testSourceDir = SIMPLECPP_TEST_SOURCE_DIR; + +namespace { + enum class Input : std::uint8_t { + Stringstream, + CharBuffer + }; +} + +static Input USE_INPUT = Input::Stringstream; static int numberOfFailedAssertions = 0; #define ASSERT_EQUALS(expected, actual) (assertEquals((expected), (actual), __LINE__)) -#define ASSERT_THROW_EQUALS(stmt, e, expected) do { try { stmt; assertThrowFailed(__LINE__); } catch (const e& ex) { assertEquals((expected), (ex.what()), __LINE__); } } while(false) +#define ASSERT_THROW_EQUALS(stmt, e, expected) do { try { stmt; assertThrowFailed(__LINE__); } catch (const e& ex) { assertEquals((expected), (ex.what()), __LINE__); } } while (false) +#define TODO_ASSERT_EQUALS(wanted, current, actual) (todoAssertEquals((wanted), (current), (actual), __LINE__)) static std::string pprint(const std::string &in) { @@ -35,12 +59,22 @@ static std::string pprint(const std::string &in) return ret; } +static const char* inputString(Input input) { + switch (input) { + case Input::Stringstream: + return "Stringstream"; + case Input::CharBuffer: + return "CharBuffer"; + } + return ""; // unreachable - needed for GCC and Visual Studio +} + static int assertEquals(const std::string &expected, const std::string &actual, int line) { if (expected != actual) { numberOfFailedAssertions++; - std::cerr << "------ assertion failed ---------" << std::endl; - std::cerr << "line " << line << std::endl; + std::cerr << "------ assertion failed (" << inputString(USE_INPUT) << ")---------" << std::endl; + std::cerr << "line test.cpp:" << line << std::endl; std::cerr << "expected:" << pprint(expected) << std::endl; std::cerr << "actual:" << pprint(actual) << std::endl; } @@ -52,6 +86,23 @@ static int assertEquals(const long long &expected, const long long &actual, int return assertEquals(std::to_string(expected), std::to_string(actual), line); } +/** + * For a known bug: actual must be the current (wrong) value. When the bug is + * fixed and actual is the wanted value, the assertion fails so that it is + * changed to ASSERT_EQUALS. + */ +static void todoAssertEquals(const std::string &wanted, const std::string ¤t, const std::string &actual, int line) +{ + if (wanted == actual) { + numberOfFailedAssertions++; + std::cerr << "------ TODO assertion fixed (" << inputString(USE_INPUT) << ")---------" << std::endl; + std::cerr << "line test.cpp:" << line << std::endl; + std::cerr << "change TODO_ASSERT_EQUALS to ASSERT_EQUALS" << std::endl; + return; + } + assertEquals(current, actual, line); +} + static void assertThrowFailed(int line) { numberOfFailedAssertions++; @@ -62,8 +113,9 @@ static void assertThrowFailed(int line) static void testcase(const std::string &name, void (*f)(), int argc, char * const *argv) { - if (argc == 1) + if (argc == 1) { f(); + } else { for (int i = 1; i < argc; i++) { if (name == argv[i]) @@ -74,46 +126,85 @@ static void testcase(const std::string &name, void (*f)(), int argc, char * cons #define TEST_CASE(F) (testcase(#F, F, argc, argv)) -static simplecpp::TokenList makeTokenList(const char code[], std::size_t size, std::vector &filenames, const std::string &filename=std::string(), simplecpp::OutputList *outputList=nullptr) +static simplecpp::TokenList makeTokenList(const char code[], std::size_t size, std::vector &filenames, const std::string &filename=std::string(), const simplecpp::DUI &dui = {}, simplecpp::OutputList *outputList = nullptr) +{ + switch (USE_INPUT) { + case Input::Stringstream: { + std::istringstream istr(std::string(code, size)); + return {istr,filenames,filename,dui,outputList}; + } + case Input::CharBuffer: + return {{code, size}, filenames, filename, dui, outputList}; + } + + return simplecpp::TokenList{filenames}; // unreachable - needed for GCC and Visual Studio +} + +static simplecpp::TokenList makeTokenList(const char code[], std::vector &filenames, const std::string &filename=std::string(), const simplecpp::DUI &dui = {}, simplecpp::OutputList *outputList=nullptr) +{ + return makeTokenList(code, strlen(code), filenames, filename, dui, outputList); +} + +static simplecpp::TokenList makeTokenList(const char code[], const simplecpp::DUI &dui = {}, simplecpp::OutputList *outputList=nullptr) { - std::istringstream istr(std::string(code, size)); - return simplecpp::TokenList(istr,filenames,filename,outputList); + std::vector files; + return makeTokenList(code, strlen(code), files, std::string(), dui, outputList); } -static simplecpp::TokenList makeTokenList(const char code[], std::vector &filenames, const std::string &filename=std::string(), simplecpp::OutputList *outputList=nullptr) +static std::string readfile(const char code[], const simplecpp::DUI &dui = {}, simplecpp::OutputList *outputList = nullptr) { - return makeTokenList(code, strlen(code), filenames, filename, outputList); + std::vector files; + return makeTokenList(code,files,std::string(),dui,outputList).stringify(); } -static std::string readfile(const char code[], simplecpp::OutputList *outputList=nullptr) +static std::string readfile(const char code[], std::size_t size, const simplecpp::DUI &dui = {}, simplecpp::OutputList *outputList=nullptr) { std::vector files; - return makeTokenList(code,files,std::string(),outputList).stringify(); + return makeTokenList(code,size,files,std::string(),dui,outputList).stringify(); } -static std::string readfile(const char code[], std::size_t size, simplecpp::OutputList *outputList=nullptr) +/** Write the code to a temporary file and read it with TokenList(filename), so FileStream is used */ +static std::string readfileFromFile(const char code[], std::size_t size, simplecpp::OutputList *outputList = nullptr) { + const std::string filename = "testrunner_filestream.tmp"; + { + std::ofstream f(filename, std::ios::binary); + f.write(code, static_cast(size)); + } std::vector files; - return makeTokenList(code,size,files,std::string(),outputList).stringify(); + std::string ret = simplecpp::TokenList(filename, files, {}, outputList).stringify(); + std::remove(filename.c_str()); + return ret; } -static std::string preprocess(const char code[], const simplecpp::DUI &dui, simplecpp::OutputList *outputList) +static std::string preprocess(const char code[], std::size_t size, const simplecpp::DUI &dui, simplecpp::OutputList *outputList, std::list *macroUsage = nullptr, std::list *ifCond = nullptr, const std::string &file = std::string()) { std::vector files; simplecpp::FileDataCache cache; - simplecpp::TokenList tokens = makeTokenList(code,files); - tokens.removeComments(); + simplecpp::TokenList tokens = makeTokenList(code, size, files, file, dui, outputList); + if (dui.removeComments) + tokens.removeComments(); simplecpp::TokenList tokens2(files); - simplecpp::preprocess(tokens2, tokens, files, cache, dui, outputList); + simplecpp::preprocess(tokens2, tokens, files, cache, dui, outputList, macroUsage, ifCond); simplecpp::cleanup(cache); return tokens2.stringify(); } +static std::string preprocess(const char code[], const simplecpp::DUI &dui, simplecpp::OutputList *outputList, std::list *macroUsage = nullptr, std::list *ifCond = nullptr, const std::string &file = std::string()) +{ + return preprocess(code, strlen(code), dui, outputList, macroUsage, ifCond, file); +} + static std::string preprocess(const char code[]) { return preprocess(code, simplecpp::DUI(), nullptr); } +static std::string preprocess(const char code[], const std::string &file) +{ + return preprocess(code, simplecpp::DUI(), nullptr, nullptr, nullptr, file); +} + static std::string preprocess(const char code[], const simplecpp::DUI &dui) { return preprocess(code, dui, nullptr); @@ -124,6 +215,21 @@ static std::string preprocess(const char code[], simplecpp::OutputList *outputLi return preprocess(code, simplecpp::DUI(), outputList); } +static std::string preprocess(const char code[], std::size_t size, simplecpp::OutputList *outputList) +{ + return preprocess(code, size, simplecpp::DUI(), outputList); +} + +static std::string preprocess(const char code[], std::list *ifCond) +{ + return preprocess(code, simplecpp::DUI(), nullptr, nullptr, ifCond); +} + +static std::string preprocess(const char code[], std::list *macroUsage) +{ + return preprocess(code, simplecpp::DUI(), nullptr, macroUsage); +} + static std::string toString(const simplecpp::OutputList &outputList) { std::ostringstream ostr; @@ -146,9 +252,21 @@ static std::string toString(const simplecpp::OutputList &outputList) case simplecpp::Output::Type::SYNTAX_ERROR: ostr << "syntax_error,"; break; + case simplecpp::Output::DIRECTIVE_AS_MACRO_PARAMETER: + ostr << "directive_as_macro_parameter,"; + break; case simplecpp::Output::Type::PORTABILITY_BACKSLASH: ostr << "portability_backslash,"; break; + case simplecpp::Output::Type::PORTABILITY_LINE_DIRECTIVE: + ostr << "portability_line_directive,"; + break; + case simplecpp::Output::Type::PORTABILITY_NO_EOF_NEWLINE: + ostr << "portability_no_eof_newline,"; + break; + case simplecpp::Output::Type::PORTABILITY_STRINGIFY: + ostr << "portability_stringify,"; + break; case simplecpp::Output::Type::UNHANDLED_CHAR_ERROR: ostr << "unhandled_char_error,"; break; @@ -168,26 +286,68 @@ static std::string toString(const simplecpp::OutputList &outputList) return ostr.str(); } +/** + * Preprocess the code. Each output token is written as str(line:col), or as + * str + '@' + macro name + (line:col) when it comes from a macro expansion. + */ +static std::string expandedTokens(const char code[], const simplecpp::DUI &dui = simplecpp::DUI(), std::list *macroUsage = nullptr) +{ + std::vector files; + simplecpp::FileDataCache cache; + simplecpp::TokenList tokens(files); + const simplecpp::TokenList rawtokens = makeTokenList(code, files); + simplecpp::preprocess(tokens, rawtokens, files, cache, dui, nullptr, macroUsage); + std::string ret; + for (const simplecpp::Token *tok = tokens.cfront(); tok; tok = tok->next) { + if (!ret.empty()) + ret += ' '; + ret += tok->str(); + if (!tok->macro.empty()) + ret += '@' + tok->macro; + ret += '(' + std::to_string(tok->location.line) + ':' + std::to_string(tok->location.col) + ')'; + } + return ret; +} + +/** Each macro usage as "name line:col use line:col", with " unknown" if the value is not known. + * The usages are sorted, the order of the macros is not specified. */ +static std::string toString(const std::list ¯oUsage) +{ + std::vector lines; + for (const simplecpp::MacroUsage &mu : macroUsage) { + std::string line = mu.macroName + ' ' + std::to_string(mu.macroLocation.line) + ':' + std::to_string(mu.macroLocation.col) + + " use " + std::to_string(mu.useLocation.line) + ':' + std::to_string(mu.useLocation.col); + if (!mu.macroValueKnown) + line += " unknown"; + lines.push_back(line + '\n'); + } + std::sort(lines.begin(), lines.end()); + std::string ret; + for (const std::string &line : lines) + ret += line; + return ret; +} + static void backslash() { // preprocessed differently simplecpp::OutputList outputList; - readfile("//123 \\\n456", &outputList); + readfile("//123 \\\n456\n", {}, &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("//123 \\ \n456", &outputList); + readfile("//123 \\ \n456\n", {}, &outputList); ASSERT_EQUALS("file0,1,portability_backslash,Combination 'backslash space newline' is not portable.\n", toString(outputList)); outputList.clear(); - readfile("#define A \\\n123", &outputList); + readfile("#define A \\\n123\n", {}, &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("#define A \\ \n123", &outputList); + readfile("#define A \\ \n123\n", {}, &outputList); ASSERT_EQUALS("file0,1,portability_backslash,Combination 'backslash space newline' is not portable.\n", toString(outputList)); } static void builtin() { - ASSERT_EQUALS("\"\" 1 0", preprocess("__FILE__ __LINE__ __COUNTER__")); + ASSERT_EQUALS("\"test.c\" 1 0", preprocess("__FILE__ __LINE__ __COUNTER__", "test.c")); ASSERT_EQUALS("\n\n3", preprocess("\n\n__LINE__")); ASSERT_EQUALS("\n\n0", preprocess("\n\n__COUNTER__")); ASSERT_EQUALS("\n\n0 1", preprocess("\n\n__COUNTER__ __COUNTER__")); @@ -250,10 +410,10 @@ static void characterLiteral() ASSERT_EQUALS('\u0012', simplecpp::characterLiteralToLL("'\\u0012'")); ASSERT_EQUALS('\U00000012', simplecpp::characterLiteralToLL("'\\U00000012'")); - ASSERT_EQUALS((static_cast(static_cast('b')) << 8) | static_cast('c'), simplecpp::characterLiteralToLL("'bc'")); + ASSERT_EQUALS((static_cast(static_cast('b')) << 8) | static_cast('c'), simplecpp::characterLiteralToLL("'bc'")); ASSERT_EQUALS((static_cast(static_cast('\x23')) << 8) | static_cast('\x45'), simplecpp::characterLiteralToLL("'\\x23\\x45'")); - ASSERT_EQUALS((static_cast(static_cast('\11')) << 8) | static_cast('\222'), simplecpp::characterLiteralToLL("'\\11\\222'")); - ASSERT_EQUALS((static_cast(static_cast('\a')) << 8) | static_cast('\b'), simplecpp::characterLiteralToLL("'\\a\\b'")); + ASSERT_EQUALS((static_cast(static_cast('\11')) << 8) | static_cast('\222'), simplecpp::characterLiteralToLL("'\\11\\222'")); + ASSERT_EQUALS((static_cast(static_cast('\a')) << 8) | static_cast('\b'), simplecpp::characterLiteralToLL("'\\a\\b'")); if (sizeof(int) <= 4) ASSERT_EQUALS(-1, simplecpp::characterLiteralToLL("'\\xff\\xff\\xff\\xff'")); else @@ -315,6 +475,8 @@ static void characterLiteral() ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u8'\343\201\202'"), std::runtime_error, "code point too large"); ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u8'\360\223\200\200'"), std::runtime_error, "code point too large"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("'\\U11111111"), std::runtime_error, "code point too large"); + ASSERT_EQUALS('\x89', simplecpp::characterLiteralToLL("'\x89'")); ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("U'\x89'"), std::runtime_error, "assumed UTF-8 encoded source, but sequence is invalid"); @@ -366,6 +528,33 @@ static void characterLiteral() ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("U'\xed\xa0\x80'"), std::runtime_error, "assumed UTF-8 encoded source, but sequence is invalid"); ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("U'\xed\xbf\xbf'"), std::runtime_error, "assumed UTF-8 encoded source, but sequence is invalid"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL(""), std::runtime_error, "expected a character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("LU"), std::runtime_error, "expected a character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL(";\n"), std::runtime_error, "expected a character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u8U"), std::runtime_error, "expected a character literal"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("'\n\n"), std::runtime_error, "raw single quotes and newlines not allowed in character literals"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("''&"), std::runtime_error, "raw single quotes and newlines not allowed in character literals"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("L'\fff"), std::runtime_error, "multiple characters only supported in narrow character literals"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("'\\\n"), std::runtime_error, "unexpected end of character literal"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("'"), std::runtime_error, "missing closing quote in character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u'"), std::runtime_error, "missing closing quote in character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("L'"), std::runtime_error, "missing closing quote in character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("'a"), std::runtime_error, "missing closing quote in character literal"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("''"), std::runtime_error, "empty character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("L''"), std::runtime_error, "empty character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("U''"), std::runtime_error, "empty character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u''"), std::runtime_error, "empty character literal"); + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u8''"), std::runtime_error, "empty character literal"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("'\\555"), std::runtime_error, "numeric escape sequence too large"); + + ASSERT_THROW_EQUALS(simplecpp::characterLiteralToLL("u'Ó"), std::runtime_error, "assumed UTF-8 encoded source, but character literal ends unexpectedly"); } static void combineOperators_floatliteral() @@ -393,6 +582,7 @@ static void combineOperators_floatliteral() ASSERT_EQUALS("1p + 3", preprocess("1p+3")); ASSERT_EQUALS("1.0_a . b", preprocess("1.0_a.b")); ASSERT_EQUALS("1_a . b", preprocess("1_a.b")); + ASSERT_EQUALS("bool x = d != 0. and b ;", preprocess("bool x = d != 0. and b;")); } static void combineOperators_increment() @@ -412,6 +602,7 @@ static void combineOperators_andequal() ASSERT_EQUALS("x &= 2 ;", preprocess("x &= 2;")); ASSERT_EQUALS("void f ( x & = 2 ) ;", preprocess("void f(x &= 2);")); ASSERT_EQUALS("f ( x &= 2 ) ;", preprocess("f(x &= 2);")); + ASSERT_EQUALS("f ( ) { return new int ( i &= 1 ) ; }", preprocess("f () { return new int(i &= 1); }")); } static void combineOperators_ellipsis() @@ -422,48 +613,75 @@ static void combineOperators_ellipsis() static void comment() { + simplecpp::DUI dui; + dui.removeComments = true; + ASSERT_EQUALS("// abc", readfile("// abc")); - ASSERT_EQUALS("", preprocess("// abc")); + ASSERT_EQUALS("", preprocess("// abc", dui)); ASSERT_EQUALS("/*\n\n*/abc", readfile("/*\n\n*/abc")); - ASSERT_EQUALS("\n\nabc", preprocess("/*\n\n*/abc")); + ASSERT_EQUALS("\n\nabc", preprocess("/*\n\n*/abc", dui)); ASSERT_EQUALS("* p = a / * b / * c ;", readfile("*p=a/ *b/ *c;")); - ASSERT_EQUALS("* p = a / * b / * c ;", preprocess("*p=a/ *b/ *c;")); + ASSERT_EQUALS("* p = a / * b / * c ;", preprocess("*p=a/ *b/ *c;", dui)); } static void comment_multiline() { + simplecpp::DUI dui; + dui.removeComments = true; + const char code[] = "#define ABC {// \\\n" "}\n" "void f() ABC\n"; - ASSERT_EQUALS("\n\nvoid f ( ) {", preprocess(code)); + ASSERT_EQUALS("\n\nvoid f ( ) {", preprocess(code, dui)); const char code1[] = "#define ABC {// \\\r\n" - "}\n" - "void f() ABC\n"; - ASSERT_EQUALS("\n\nvoid f ( ) {", preprocess(code1)); + "}\n" + "void f() ABC\n"; + ASSERT_EQUALS("\n\nvoid f ( ) {", preprocess(code1, dui)); const char code2[] = "#define A 1// \\\r" - "\r" - "2\r" - "A\r"; - ASSERT_EQUALS("\n\n2\n1", preprocess(code2)); + "\r" + "2\r" + "A\r"; + ASSERT_EQUALS("\n\n2\n1", preprocess(code2, dui)); const char code3[] = "void f() {// \\ \n}\n"; - ASSERT_EQUALS("void f ( ) {", preprocess(code3)); + ASSERT_EQUALS("void f ( ) {", preprocess(code3, dui)); const char code4[] = "void f() {// \\\\\\\t\t\n}\n"; - ASSERT_EQUALS("void f ( ) {", preprocess(code4)); + ASSERT_EQUALS("void f ( ) {", preprocess(code4, dui)); const char code5[] = "void f() {// \\\\\\a\n}\n"; - ASSERT_EQUALS("void f ( ) {\n}", preprocess(code5)); + ASSERT_EQUALS("void f ( ) {\n}", preprocess(code5, dui)); const char code6[] = "void f() {// \\\n\n\n}\n"; - ASSERT_EQUALS("void f ( ) {\n\n\n}", preprocess(code6)); + ASSERT_EQUALS("void f ( ) {\n\n\n}", preprocess(code6, dui)); // #471 ensure there is newline in comment so that line-splicing can be detected by tools ASSERT_EQUALS("// abc\ndef", readfile("// abc\\\ndef")); } +static void keep_comments() +{ + simplecpp::DUI dui; + dui.removeComments = false; + + { + const char code[] = "/* comment */\n"; + ASSERT_EQUALS("/* comment */", preprocess(code,dui)); + } + + { + const char code[] = "// comment\n"; + ASSERT_EQUALS("// comment", preprocess(code,dui)); + } + + { + const char code[] = "#define MACRO /* comment */\nMACRO\n"; + ASSERT_EQUALS("\n/* comment */", preprocess(code,dui)); + } +} + static void constFold() { @@ -479,6 +697,22 @@ static void constFold() ASSERT_EQUALS("1", testConstFold("010==8")); ASSERT_EQUALS("exception", testConstFold("!1 ? 2 :")); ASSERT_EQUALS("exception", testConstFold("?2:3")); + ASSERT_EQUALS("0", testConstFold("0 && 1/0")); + ASSERT_EQUALS("1", testConstFold("1 || 1/0")); + ASSERT_EQUALS("2", testConstFold("1 ? 2 : 1/0")); + ASSERT_EQUALS("exception", testConstFold("1 && 1/0")); + ASSERT_EQUALS("exception", testConstFold("1/0 && 0")); + ASSERT_EQUALS("exception", testConstFold("(1/0) ? 1 : 2")); + ASSERT_EQUALS("exception", testConstFold("-(1/0) + 1")); + // ?: is right-associative + ASSERT_EQUALS("1", testConstFold("1 ? 1 : 0 ? 2 : 3")); + ASSERT_EQUALS("3", testConstFold("0 ? 1 : 0 ? 2 : 3")); + ASSERT_EQUALS("2", testConstFold("1 ? 2 : 3 ? 4 : 5 ? 6 : 7")); + ASSERT_EQUALS("7", testConstFold("0 ? 2 : 0 ? 4 : 0 ? 6 : 7")); + ASSERT_EQUALS("6", testConstFold("1 ? 0 ? 5 : 6 : 7")); + ASSERT_EQUALS("2", testConstFold("(1 ? 1 : 0) ? 2 : 3")); + ASSERT_EQUALS("1", testConstFold("1 ? 1 : (1/0) ? 2 : 3")); + ASSERT_EQUALS("exception", testConstFold("0 ? 1 : (1/0) ? 2 : 3")); } #ifdef __CYGWIN__ @@ -565,9 +799,12 @@ static void define6() static void define7() { + simplecpp::DUI dui; + dui.removeComments = true; + const char code[] = "#define A(X) X+1\n" "A(1 /*23*/)"; - ASSERT_EQUALS("\n1 + 1", preprocess(code)); + ASSERT_EQUALS("\n1 + 1", preprocess(code, dui)); } static void define8() // 6.10.3.10 @@ -632,6 +869,270 @@ static void define13() "}", preprocess(code)); } +static void define14() // #296 +{ + const char code[] = "#define bar(x) x % 2\n" + "#define foo(x) printf(#x \"\\n\")\n" + "\n" + " foo(bar(3));\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "printf ( \"bar(3)\" \"\\n\" ) ;", preprocess(code)); +} + +static void define15() // #231 +{ + const char code[] = "#define CAT(a, b) CAT2(a, b)\n" + "#define CAT2(a, b) a ## b\n" + "#define FOO x\n" + "#define BAR() CAT(F, OO)\n" + "#define BAZ CAT(B, AR)()\n" + "BAZ\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "x", preprocess(code)); +} + +static void define16() // #201 +{ + const char code[] = "#define ALL_COLORS(warm_colors) \\\n" + " X(Blue) \\\n" + " X(Green) \\\n" + " X(Purple) \\\n" + " warm_colors\n" + "\n" + "#define WARM_COLORS \\\n" + " X(Red) \\\n" + " X(Yellow) \\\n" + " X(Orange)\n" + "\n" + "#define COLOR_SET ALL_COLORS(WARM_COLORS)\n" + "\n" + "#define X(color) #color,\n" + "\n" + "COLOR_SET\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\"Blue\" , \"Green\" , \"Purple\" , \"Red\" , \"Yellow\" , \"Orange\" ,", preprocess(code)); +} + +static void define17() // #185 +{ + const char code[] = "#define at(x, y) x##y\n" + "#define b(...) \\\n" + "aa(__VA_ARGS__, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , \\\n" + ", , , , , , , , 2)\n" + "#define aa(c, d, a, b, e, f, g, h, ab, ac, i, ad, j, k, l, m, n, o, p, ae, q, \\\n" + "r, s, t, u, v, w, x, y, z, af, ag, ah, ai, aj, ak, al, am, an, ao, \\\n" + "ap) \\\n" + "ap\n" + "#define aq(...) ar(b(__VA_ARGS__), __VA_ARGS__) static_assert(true, \" \")\n" + "#define ar(ap, ...) at(I_, ap)(__VA_ARGS__)\n" + "#define I_2(as, a)\n" + "aq(a, array);\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "static_assert ( true , \" \" ) ;", preprocess(code)); +} + +static void define18() // #130 +{ + const char code[] = "#define MAC2STR(x) x[0],x[1],x[2],x[3],x[4],x[5]\n" + "#define FT_DEBUG(fmt, args...) if(pGlobalCtx && pGlobalCtx->debug_level>=2) printf(\"FT-dbg: \"fmt, ##args)\n" + "\n" + "FT_DEBUG(\" %02x:%02x:%02x:%02x:%02x:%02x\\n\", MAC2STR(pCtx->wlan_intf_addr[i]));\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "if ( pGlobalCtx && pGlobalCtx -> debug_level >= 2 ) printf ( \"FT-dbg: \" \" %02x:%02x:%02x:%02x:%02x:%02x\\n\" , pCtx -> wlan_intf_addr [ i ] [ 0 ] , pCtx -> wlan_intf_addr [ i ] [ 1 ] , pCtx -> wlan_intf_addr [ i ] [ 2 ] , pCtx -> wlan_intf_addr [ i ] [ 3 ] , pCtx -> wlan_intf_addr [ i ] [ 4 ] , pCtx -> wlan_intf_addr [ i ] [ 5 ] ) ;", preprocess(code)); +} + +static void define19() // #124 +{ + const char code[] = "#define CONCAT(tok) tok##suffix\n" + "\n" + "CONCAT(Test);\n" + "CONCAT(const Test);\n"; + ASSERT_EQUALS("\n" + "\n" + "Testsuffix ;\n" + "const Testsuffix ;", preprocess(code)); +} + +static void define20() // #113 +{ + const char code[] = "#define TARGS4 T1,T2,T3,T4\n" + "#define FOOIMPL(T__CLASS, TARGS) void foo(const T__CLASS& x) { }\n" + "#define FOOIMPL_4(T__CLASS) FOOIMPL(T__CLASS, TARGS4)\n" + "FOOIMPL_4(y)\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "void foo ( const y < T1 , T2 , T3 , T4 > & x ) { }", preprocess(code)); +} + +static void define21() // #66 +{ + const char code[] = "#define GETMYID(a) ((a))+1\n" + "#define FIGHT_FOO(c, ...) foo(c, ##__VA_ARGS__)\n" + "FIGHT_FOO(1, GETMYID(a));\n"; + ASSERT_EQUALS("\n" + "\n" + "foo ( 1 , ( ( a ) ) + 1 ) ;", preprocess(code)); +} + +static void define22() // #40 +{ + const char code[] = "#define COUNTER_NAME(NAME, ...) NAME##Count\n" + "#define COMMA ,\n" + "\n" + "#define DECLARE_COUNTERS(LIST) unsigned long LIST(COUNTER_NAME, COMMA);\n" + "\n" + "#define ACTUAL_LIST(FUNCTION, SEPARATOR) \\\n" + "FUNCTION(event1, int, foo) SEPARATOR \\\n" + "FUNCTION(event2, char, bar)\n" + "\n" + "DECLARE_COUNTERS(ACTUAL_LIST)\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "unsigned long event1Count , event2Count ;", preprocess(code)); +} + +static void define23() // #40 +{ + const char code[] = "#define COMMA ,\n" + "#define MULTI(SEPARATOR) A SEPARATOR B\n" + "\n" + "#define VARS MULTI(COMMA)\n" + "unsigned VARS;\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "unsigned A , B ;", preprocess(code)); +} + +static void define24() +{ + // an expansion result that is a function-like macro name must be rescanned + // repeatedly against the tokens that follow it + const char code[] = "#define a(b, c) c\n" + "#define d() a\n" + "#define g(e) h(e, ) h(e, )\n" + "#define h(e, b) d()(, e)()\n" + "#define i()\n" + "g(i)\n"; + ASSERT_EQUALS("", preprocess(code)); +} + +static void define25() +{ + // a macro name that came from expanding that same macro must not be + // re-expanded when rescanned with the tokens that follow it + const char code[] = "#define f() f\n" + "#define wrap(x) x()\n" + "wrap(f())\n"; + ASSERT_EQUALS("\n" + "\n" + "f ( )", preprocess(code)); +} + +static void define26() +{ + // a macro name that came from expanding that same macro must not be + // re-expanded with arguments taken from the raw token stream + const char code[] = "#define f() f\n" + "f()()\n"; + ASSERT_EQUALS("\n" + "f ( )", preprocess(code)); + + const char code2[] = "#define f() f\n" + "f()()()\n"; + ASSERT_EQUALS("\n" + "f ( ) ( )", preprocess(code2)); +} + +static void define27() +{ + // an expansion result inside a macro argument that is a function-like macro + // name must be rescanned against the remaining argument tokens + const char code1[] = "#define f(x) [x]\n" + "#define ID(x) x\n" + "#define G() f\n" + "ID(G()(1))\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "[ 1 ]", preprocess(code1)); + + // the rescanned call's arguments are themselves rescanned + const char code2[] = "#define f(x) [x]\n" + "#define ID(x) x\n" + "#define G() f\n" + "ID(G()(G()(2)))\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "[ [ 2 ] ]", preprocess(code2)); + + // each rescan round must use the right set of already-expanded macros: + // A(1)->B, B(2)->A, A(3)->B, B(4)->A + const char code3[] = "#define ID(x) x\n" + "#define A(x) B\n" + "#define B(x) A\n" + "ID(A(1)(2)(3)(4))\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "A", preprocess(code3)); + + // creduce'd fuzzer case: the missed rescan of o1(k) deferred the call to a + // context where j was wrongly blocked from expanding, so b pasted ')' and + // '1' into an invalid token that broke the parentheses of the m call + const char code4[] = "#define o1(a) a()\n" + "#define b(c, d) c##d\n" + "#define l(c, d) b(c, 1)\n" + "#define m(e, f)\n" + "#define g(h, i) m(h, )\n" + "#define j(h, i) g(h, )\n" + "#define k() l(j(, ), )\n" + "#define n(a) j(l(o, )(a), )\n" + "n(k)\n"; + ASSERT_EQUALS("", preprocess(code4)); +} static void define_invalid_1() @@ -639,15 +1140,71 @@ static void define_invalid_1() const char code[] = "#define A(\nB\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); - ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define\n", toString(outputList)); + ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, bad macro syntax\n", toString(outputList)); } static void define_invalid_2() { - const char code[] = "#define\nhas#"; + const char code[] = "#define\nhas#\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); - ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define\n", toString(outputList)); + ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, bad macro syntax\n", toString(outputList)); +} + +static void define_invalid_3() +{ + const char code[] = "#define R()\n" + "R\n"; + simplecpp::OutputList outputList; + // TODO: a function-like macro name at the end of the file is not a macro call, gcc outputs R + TODO_ASSERT_EQUALS("\nR", "", preprocess(code, &outputList)); + TODO_ASSERT_EQUALS("", "file0,2,syntax_error,failed to expand 'R', Wrong number of parameters for macro 'R'.\n", toString(outputList)); +} + +static void define_wrong_number_of_arguments() +{ + { + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("#define f(a,b) a+b\nf(1)\n", &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to expand 'f', Wrong number of parameters for macro 'f'.\n", toString(outputList)); + } + { + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("#define f(a,b) a+b\nf(1,2,3)\n", &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to expand 'f', Wrong number of parameters for macro 'f'.\n", toString(outputList)); + } + { + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("#define f(a,b,...) a+b\nf(1)\n", &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to expand 'f', Wrong number of parameters for macro 'f'.\n", toString(outputList)); + } + { + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n1", preprocess("#define f(a,...) a __VA_ARGS__\nf(1)\n", &outputList)); + ASSERT_EQUALS("", toString(outputList)); + } + { + // a function-like macro name that is not followed by ( is not expanded + simplecpp::OutputList outputList; + ASSERT_EQUALS("\nf ;", preprocess("#define f(a) a\nf;\n", &outputList)); + ASSERT_EQUALS("", toString(outputList)); + } + { + // TODO: f takes no arguments, gcc reports an error + simplecpp::OutputList outputList; + TODO_ASSERT_EQUALS("", "\n1", preprocess("#define f() 1\nf(1)\n", &outputList)); + TODO_ASSERT_EQUALS("file0,2,syntax_error,failed to expand 'f', Wrong number of parameters for macro 'f'.\n", "", toString(outputList)); + } +} + +static void define_invalid_4() +{ + const char code[] = "#define X(...)\n" + "X\n"; + simplecpp::OutputList outputList; + // TODO: a function-like macro name at the end of the file is not a macro call, gcc outputs X + TODO_ASSERT_EQUALS("\nX", "", preprocess(code, &outputList)); + TODO_ASSERT_EQUALS("", "file0,2,syntax_error,failed to expand 'X', Wrong number of parameters for macro 'X'.\n", toString(outputList)); } static void define_define_1() @@ -887,6 +1444,16 @@ static void define_define_23() // #403 crash (infinite recursion) ASSERT_EQUALS("\n\n\n\nYdieZ ( void ) ;", preprocess(code)); } +static void define_define_24() // #590 +{ + const char code[] = "#define B A\n" + "#define A x(B)\n" + "#define C(s) s\n" + "#define D(s) C(s)\n" + "D(A)\n"; + ASSERT_EQUALS("\n\n\n\nx ( A )", preprocess(code)); +} + static void define_va_args_1() { const char code[] = "#define A(fmt...) dostuff(fmt)\n" @@ -944,7 +1511,7 @@ static void define_va_opt_3() // non-escaped newline without closing parenthesis const char code1[] = "#define err(...) __VA_OPT__(printf( __VA_ARGS__);\n" ")\n" - "err()"; + "err()\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code1, &outputList)); @@ -956,7 +1523,7 @@ static void define_va_opt_3() // non-escaped newline without open parenthesis const char code2[] = "#define err(...) __VA_OPT__\n" "(something)\n" - "err()"; + "err()\n"; ASSERT_EQUALS("", preprocess(code2, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, In definition of 'err': Missing opening parenthesis for __VA_OPT__\n", @@ -967,7 +1534,7 @@ static void define_va_opt_4() { // missing parenthesis const char code1[] = "#define err(...) __VA_OPT__ something\n" - "err()"; + "err()\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code1, &outputList)); @@ -978,7 +1545,7 @@ static void define_va_opt_4() // missing open parenthesis const char code2[] = "#define err(...) __VA_OPT__ something)\n" - "err()"; + "err()\n"; ASSERT_EQUALS("", preprocess(code2, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, In definition of 'err': Missing opening parenthesis for __VA_OPT__\n", @@ -989,7 +1556,7 @@ static void define_va_opt_5() { // parenthesis not directly proceeding __VA_OPT__ const char code[] = "#define err(...) __VA_OPT__ something (something)\n" - "err()"; + "err()\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); @@ -1001,7 +1568,7 @@ static void define_va_opt_6() { // nested __VA_OPT__ const char code[] = "#define err(...) __VA_OPT__(__VA_OPT__(something))\n" - "err()"; + "err()\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); @@ -1012,7 +1579,7 @@ static void define_va_opt_6() static void define_va_opt_7() { // eof in __VA_OPT__ - const char code1[] = "#define err(...) __VA_OPT__"; + const char code1[] = "#define err(...) __VA_OPT__\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code1, &outputList)); @@ -1021,7 +1588,7 @@ static void define_va_opt_7() outputList.clear(); - const char code2[] = "#define err(...) __VA_OPT__("; + const char code2[] = "#define err(...) __VA_OPT__(\n"; ASSERT_EQUALS("", preprocess(code2, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, In definition of 'err': Missing closing parenthesis for __VA_OPT__\n", @@ -1029,25 +1596,68 @@ static void define_va_opt_7() outputList.clear(); - const char code3[] = "#define err(...) __VA_OPT__(x"; + const char code3[] = "#define err(...) __VA_OPT__(x\n"; ASSERT_EQUALS("", preprocess(code3, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,Failed to parse #define, In definition of 'err': Missing closing parenthesis for __VA_OPT__\n", toString(outputList)); } -static void define_ifdef() +static void define_va_opt_8() { - const char code[] = "#define A(X) X\n" - "A(1\n" - "#ifdef CFG\n" - "#endif\n" - ")\n"; + const char code[] = "#define f(...) #__VA_OPT__(x)\n" + "const char* v1 = f();\n"; simplecpp::OutputList outputList; - ASSERT_EQUALS("", preprocess(code, &outputList)); - ASSERT_EQUALS("file0,3,syntax_error,failed to expand 'A', it is invalid to use a preprocessor directive as macro parameter\n", toString(outputList)); - + ASSERT_EQUALS("\nconst char * v1 = \"\" ;", preprocess(code, &outputList)); + ASSERT_EQUALS("", toString(outputList)); +} + +static void define_va_opt_9() +{ + simplecpp::DUI dui; + dui.defines.emplace_back("f(...)=__VA_OPT__"); + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("", dui, &outputList)); + ASSERT_EQUALS("file0,0,dui_error,In definition of 'f': Missing opening parenthesis for __VA_OPT__\n", toString(outputList)); +} + +static void define_ifdef() +{ + const char code[] = "#define A(X) X\n" + "A(1\n" + "#ifdef CFG\n" + "#endif\n" + ")\n"; + + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,3,directive_as_macro_parameter,failed to expand 'A', it is invalid to use a preprocessor directive as macro parameter\n", toString(outputList)); + +} + +static void if_invalid_1() +{ + const char code[] = "#if'\\u'\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, expected digit\n", toString(outputList)); +} + +static void if_invalid_2() +{ + const char code[] = "#if-0xBBB4444444444444%~B\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, division overflow\n", toString(outputList)); +} + +static void if_invalid_3() +{ + const char code[] = "#if@u'\\udefa'\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, surrogate code points not allowed in universal character names\n", toString(outputList)); } static void pragma_backslash() @@ -1065,6 +1675,15 @@ static void pragma_backslash() ASSERT_EQUALS("", preprocess(code, &outputList)); } +static void pragma_backslash_2() // #217 +{ + const char code[] = "#pragma comment(linker, \"foo \\\n" + "bar\")\n"; + + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); +} + static void dollar() { ASSERT_EQUALS("$ab", readfile("$ab")); @@ -1093,7 +1712,7 @@ static void error3() " bla bla.\"\n"; std::vector files; simplecpp::OutputList outputList; - const simplecpp::TokenList rawtokens = makeTokenList(code, files, "test.c", &outputList); + const simplecpp::TokenList rawtokens = makeTokenList(code, files, "test.c", {}, &outputList); ASSERT_EQUALS("", toString(outputList)); } @@ -1105,7 +1724,7 @@ static void error4() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtoken = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtoken = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtoken, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error x\n", toString(outputList)); } @@ -1118,11 +1737,48 @@ static void error5() simplecpp::FileDataCache cache; simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); - const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code),files,"test.c"); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,#error,#error x\n", toString(outputList)); } +static void error6() +{ + // "#error\" + const char code[] = "\xFF\xFE\x23\x00\x65\x00\x72\x00\x72\x00\x6f\x00\x72\x00\x20\x00\x5c\x00\x0a\x00"; + std::vector files; + simplecpp::FileDataCache cache; + simplecpp::OutputList outputList; + simplecpp::TokenList tokens2(files); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); + simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,#error,#error \n", toString(outputList)); +} + +static void error7() +{ + const char code[] = "#error bla\\\nbla\n"; + std::vector files; + simplecpp::FileDataCache cache; + simplecpp::OutputList outputList; + simplecpp::TokenList tokens2(files); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); + simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,#error,#error blabla\n", toString(outputList)); +} + +static void error8() +{ + const char code[] = "#error bla\\\r\nbla\n"; + std::vector files; + simplecpp::FileDataCache cache; + simplecpp::OutputList outputList; + simplecpp::TokenList tokens2(files); + const simplecpp::TokenList rawtokens = makeTokenList(code, sizeof(code)-1,files,"test.c"); + simplecpp::preprocess(tokens2, rawtokens, files, cache, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,#error,#error blabla\n", toString(outputList)); +} + static void garbage() { simplecpp::OutputList outputList; @@ -1187,6 +1843,141 @@ static void hash() "STR(X(Y,Y))")); } +static void hashEscape() +{ + // a \ is only inserted before " and \ inside string literals and character constants + const char code[] = "#define str(x) #x\n" + "str(\\n)\n" + "str(: @\\n)\n" + "str(\"\\n\")\n" + "str('\\4')\n" + "str('\"')\n" + "str(L\"a\\\"b\")"; + ASSERT_EQUALS("\n" + "\"\\n\"\n" + "\": @\\n\"\n" + "\"\\\"\\\\n\\\"\"\n" + "\"'\\\\4'\"\n" + "\"'\\\"'\"\n" + "\"L\\\"a\\\\\\\"b\\\"\"", preprocess(code)); +} + +static void hashComment() +{ + // a comment is replaced by one space character + simplecpp::DUI dui; + dui.removeComments = true; + const char code[] = "#define str(x) #x\n" + "#define xstr(x) str(x)\n" + "str(a/**/b)\n" + "str(a/**/ /**/b)\n" + "str(a/**/)\n" + "xstr(a/**/b)"; + ASSERT_EQUALS("\n\n" + "\"a b\"\n" + "\"a b\"\n" + "\"a\"\n" + "\"a b\"", preprocess(code, dui)); + + // comments are kept in the string when they are not removed, like gcc -C + ASSERT_EQUALS("\n\n" + "\"a/**/b\"\n" + "\"a/**/ /**/b\"\n" + "\"a/**/\"\n" + "\"a/**/b\"", preprocess(code)); +} + +static void hashMacroTrailingWhitespace() +{ + // whitespace at the end of a replacement list does not count + simplecpp::DUI dui; + dui.removeComments = true; + const char code[] = "#define str(x) #x\n" + "#define xstr(x) str(x)\n" + "#define MAJOR 1/*major*/\n" + "#define MINOR 2 \n" + "#define F(x) x\n" + "#define G F(a)\n" + "xstr(MAJOR.MINOR)\n" + "xstr(F(a)b)\n" + "xstr(F(a) b)\n" + "xstr(G.b)\n" + "xstr(G b)"; + ASSERT_EQUALS("\n\n\n\n\n\n" + "\"1.2\"\n" + "\"ab\"\n" + "\"a b\"\n" + "\"a.b\"\n" + "\"a b\"", preprocess(code, dui)); +} + +static void hashCombinedOperator() +{ + // the whitespace after an operator of two characters is kept + const char code[] = "#define str(x) #x\n" + "str(a == 0) str(a -> b) str(x... y)\n"; + ASSERT_EQUALS("\n\"a == 0\" \"a -> b\" \"x... y\"", preprocess(code)); +} + +static void hashEscapeTrailingBackslash() +{ + // an odd number of trailing backslashes would escape the closing quote, + // the final backslash is dropped (like gcc and clang) and a warning is reported + const char msg[] = "Invalid string literal when expanding 'str', ignoring final '\\'. " + "Undefined behavior according to C standard chapter 6.10.3.2, paragraph 2.\n"; + { + const char code[] = "int x;\n" + "#define str(x) #x\n" + "str(\\)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("int x ;\n\n\"\"", preprocess(code, &outputList)); + ASSERT_EQUALS(std::string("file0,3,portability_stringify,") + msg, toString(outputList)); + } + { + const char code[] = "#define str(x) #x\n" + "str(a\\)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\"a\"", preprocess(code, &outputList)); + ASSERT_EQUALS(std::string("file0,2,portability_stringify,") + msg, toString(outputList)); + } + { + // three backslashes + const char code[] = "#define str(x) #x\n" + "str(\\\\\\)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\"\\\\\"", preprocess(code, &outputList)); + ASSERT_EQUALS(std::string("file0,2,portability_stringify,") + msg, toString(outputList)); + } + { + // # in a nested macro + const char code[] = "#define str(x) #x\n" + "#define T(x) str(x)\n" + "T(\\)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\n\"\"", preprocess(code, &outputList)); + ASSERT_EQUALS(std::string("file0,3,portability_stringify,") + msg, toString(outputList)); + } + { + // in a #if expression + const char code[] = "#define str(x) #x\n" + "#if 1 || str(\\)\n" + "A\n" + "#endif\n"; + simplecpp::OutputList outputList; + preprocess(code, &outputList); + ASSERT_EQUALS(std::string("file0,2,portability_stringify,") + msg + + "file0,2,syntax_error,failed to evaluate #if condition, invalid expression\n", toString(outputList)); + } + { + // even number of backslashes is fine + const char code[] = "#define str(x) #x\n" + "str(\\\\)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\"\\\\\"", preprocess(code, &outputList)); + ASSERT_EQUALS("", toString(outputList)); + } +} + static void hashhash1() // #4703 { const char code[] = "#define MACRO( A, B, C ) class A##B##C##Creator {};\n" @@ -1299,19 +2090,19 @@ static void hashhash9() simplecpp::OutputList outputList; code = "#define A +##x\n" - "A"; + "A\n"; outputList.clear(); ASSERT_EQUALS("", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '+' and 'x' yields an invalid token.\n", toString(outputList)); code = "#define A 2##=\n" - "A"; + "A\n"; outputList.clear(); ASSERT_EQUALS("", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '2' and '=' yields an invalid token.\n", toString(outputList)); code = "#define A <<##x\n" - "A"; + "A\n"; outputList.clear(); ASSERT_EQUALS("", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '<<' and 'x' yields an invalid token.\n", toString(outputList)); @@ -1480,15 +2271,15 @@ static void hashhash_int_literal() static void hashhash_invalid_1() { - const char code[] = "#define f(a) (##x)\nf(1)"; + const char code[] = "#define f(a) (##x)\nf(1)\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); - ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'f', Invalid ## usage when expanding 'f': Unexpected token '('\n", toString(outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'f', Invalid ## usage when expanding 'f': Combining '(' and 'x' yields an invalid token.\n", toString(outputList)); } static void hashhash_invalid_2() { - const char code[] = "#define f(a) (x##)\nf(1)"; + const char code[] = "#define f(a) (x##)\nf(1)\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'f', Invalid ## usage when expanding 'f': Unexpected token ')'\n", toString(outputList)); @@ -1497,7 +2288,7 @@ static void hashhash_invalid_2() static void hashhash_invalid_string_number() { const char code[] = - "#define BAD(x) x##12345\nBAD(\"ABC\")"; + "#define BAD(x) x##12345\nBAD(\"ABC\")\n"; simplecpp::OutputList outputList; preprocess(code, simplecpp::DUI(), &outputList); @@ -1507,7 +2298,7 @@ static void hashhash_invalid_string_number() static void hashhash_invalid_missing_args() { const char code[] = - "#define BAD(x) ##x\nBAD()"; + "#define BAD(x) ##x\nBAD()\n"; simplecpp::OutputList outputList; preprocess(code, simplecpp::DUI(), &outputList); @@ -1537,15 +2328,266 @@ static void hashhash_empty_va_args() ASSERT_EQUALS("\n\n\n( 2 )", preprocess(code)); } +static void hashhash_punctuator() // #724 +{ + const char code[] = + "#define cat(a,b) a##b\n" + "#define MINUS_GT -##>\n" + "#define VA(...) <##__VA_ARGS__\n" + "cat(+,=) cat(<<,=) cat(-,>) cat(&,&) cat(|,|) cat(+,+) cat(<,=) cat(.,5) MINUS_GT VA(<) VA()\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\n\n+= <<= -> && || ++ <= .5 -> << <", preprocess(code, &outputList)); + ASSERT_EQUALS("", toString(outputList)); +} + +static void hashhash_punctuator_invalid() +{ + const char code[] = + "#define cat(a,b) a##b\n" + "cat(+,x)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'cat', Invalid ## usage when expanding 'cat': Combining '+' and 'x' yields an invalid token.\n", toString(outputList)); +} + +static void macro_c11_example3() // C11 6.10.3.5 EXAMPLE 3 +{ + const char code[] = + "#define x 3\n" + "#define f(a) f(x * (a))\n" + "#undef x\n" + "#define x 2\n" + "#define g f\n" + "#define z z[0]\n" + "#define h g(~\n" + "#define m(a) a(w)\n" + "#define w 0,1\n" + "#define t(a) a\n" + "#define p() int\n" + "#define q(x) x\n" + "#define r(x,y) x ## y\n" + "#define str(x) # x\n" + "f(y+1) + f(f(z)) % t(t(g)(0) + t)(1);\n" + "g(x+(3,4)-w) | h 5) & m\n" + "(f)^m(m);\n" + "p() i[q()] = { q(1), r(2,3), r(4,), r(,5), r(,) };\n" + "char c[2][6] = { str(hello), str() };\n"; + // TODO: r(,) leaves an empty token + TODO_ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "f ( 2 * ( y + 1 ) ) + f ( 2 * ( f ( 2 * ( z [ 0 ] ) ) ) ) % f ( 2 * ( 0 ) ) + t ( 1 ) ;\n" + "f ( 2 * ( 2 + ( 3 , 4 ) - 0 , 1 ) ) | f ( 2 * ( ~ 5 ) ) & f ( 2 * ( 0 , 1 ) )\n" + "^ m ( 0 , 1 ) ;\n" + "int i [ ] = { 1 , 23 , 4 , 5 , } ;\n" + "char c [ 2 ] [ 6 ] = { \"hello\" , \"\" } ;", + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "f ( 2 * ( y + 1 ) ) + f ( 2 * ( f ( 2 * ( z [ 0 ] ) ) ) ) % f ( 2 * ( 0 ) ) + t ( 1 ) ;\n" + "f ( 2 * ( 2 + ( 3 , 4 ) - 0 , 1 ) ) | f ( 2 * ( ~ 5 ) ) & f ( 2 * ( 0 , 1 ) )\n" + "^ m ( 0 , 1 ) ;\n" + "int i [ ] = { 1 , 23 , 4 , 5 , } ;\n" + "char c [ 2 ] [ 6 ] = { \"hello\" , \"\" } ;", + preprocess(code)); +} + +static void macro_c11_example4() // C11 6.10.3.5 EXAMPLE 4 +{ + const char code[] = + "#define str(s) # s\n" + "#define xstr(s) str(s)\n" + "#define debug(s, t) printf(\"x\" # s \"= %d, x\" # t \"= %s\", \\\n" + " x ## s, x ## t)\n" + "#define INCFILE(n) vers ## n\n" + "#define glue(a, b) a ## b\n" + "#define xglue(a, b) glue(a, b)\n" + "#define HIGHLOW \"hello\"\n" + "#define LOW LOW \", world\"\n" + "debug(1, 2);\n" + "fputs(str(strncmp(\"abc\\0d\", \"abc\", '\\4') // this goes away\n" + " == 0) str(: @\\n), s);\n" + "xstr(INCFILE(2).h)\n" + "glue(HIGH, LOW);\n" + "xglue(HIGH, LOW)\n"; + // the comments are removed before preprocessing, like cppcheck and the simplecpp program do + simplecpp::DUI dui; + dui.removeComments = true; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "\n" + "printf ( \"x\" \"1\" \"= %d, x\" \"2\" \"= %s\" , x1 , x2 ) ;\n" + "fputs ( \"strncmp(\\\"abc\\\\0d\\\", \\\"abc\\\", '\\\\4') == 0\"\n" + "\": @\\n\" , s ) ;\n" + "\"vers2.h\"\n" + "\"hello\" ;\n" + "\"hello\" \", world\"", + preprocess(code, dui)); +} + +static void macro_c11_example5() // C11 6.10.3.5 EXAMPLE 5 +{ + const char code[] = + "#define t(x,y,z) x ## y ## z\n" + "int j[] = { t(1,2,3), t(,4,5), t(6,,7), t(8,9,),\n" + " t(10,,), t(,11,), t(,,12), t(,,) };\n"; + // TODO: t(,,) leaves an empty token + TODO_ASSERT_EQUALS("\n" + "int j [ ] = { 123 , 45 , 67 , 89 ,\n" + "10 , 11 , 12 , } ;", + "\n" + "int j [ ] = { 123 , 45 , 67 , 89 ,\n" + "10 , 11 , 12 , } ;", + preprocess(code)); +} + +static void macro_c11_example7() // C11 6.10.3.5 EXAMPLE 7 +{ + const char code[] = + "#define debug(...) fprintf(stderr, __VA_ARGS__)\n" + "#define showlist(...) puts(#__VA_ARGS__)\n" + "#define report(test, ...) ((test)?puts(#test):\\\n" + " printf(__VA_ARGS__))\n" + "debug(\"Flag\");\n" + "debug(\"X = %d\\n\", x);\n" + "showlist(The first, second, and third items.);\n" + "report(x>y, \"x is %d but y is %d\", x, y);\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "fprintf ( stderr , \"Flag\" ) ;\n" + "fprintf ( stderr , \"X = %d\\n\" , x ) ;\n" + "puts ( \"The first, second, and third items.\" ) ;\n" + "( ( x > y ) ? puts ( \"x>y\" ) : printf ( \"x is %d but y is %d\" , x , y ) ) ;", + preprocess(code)); +} + +static void macro_c23_va_opt_examples() // C23 6.10.5.1 EXAMPLE +{ + const char code[] = + "#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)\n" + "#define G(X, ...) f(0, X __VA_OPT__(,) __VA_ARGS__)\n" + "#define SDEF(sname, ...) S sname __VA_OPT__(= { __VA_ARGS__ })\n" + "#define EMP\n" + "F(a,b,c)\n" + "F()\n" + "F(EMP)\n" + "G(a,b,c)\n" + "G(a,)\n" + "G(a)\n" + "SDEF(foo);\n" + "SDEF(bar, 1, 2);\n"; + ASSERT_EQUALS("\n" + "\n" + "\n" + "\n" + "f ( 0 , a , b , c )\n" + "f ( 0 )\n" + "f ( 0 )\n" + "f ( 0 , a , b , c )\n" + "f ( 0 , a )\n" + "f ( 0 , a )\n" + "S foo ;\n" + "S bar = { 1 , 2 } ;", + preprocess(code)); +} + +static void hashhash_empty_placemarker() +{ + const char code[] = + "#define r(x,y) x ## y\n" + "{ r(,) }\n"; + std::vector files; + simplecpp::FileDataCache cache; + simplecpp::TokenList tokens(files); + const simplecpp::TokenList rawtokens = makeTokenList(code, files); + simplecpp::preprocess(tokens, rawtokens, files, cache, simplecpp::DUI()); + std::string str; + for (const simplecpp::Token *tok = tokens.cfront(); tok; tok = tok->next) + str += "[" + tok->str() + "]"; + // TODO: pasting two empty arguments gives an empty token + TODO_ASSERT_EQUALS("[{][}]", "[{][][}]", str); +} + +static void hashhash_invalid_dotdot() +{ + const char code[] = + "#define cat(a,b) a##b\n" + "cat(.,.)\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'cat', Invalid ## usage when expanding 'cat': Combining '.' and '.' yields an invalid token.\n", toString(outputList)); +} + +static void hashhash_invalid_name_string() +{ + const char code[] = + "#define cat(a,b) a##b\n" + "cat(a,\"s\")\n"; + simplecpp::OutputList outputList; + // TODO: a"s" is not a valid token, gcc reports an error + TODO_ASSERT_EQUALS("", "\na\"s\"", preprocess(code, &outputList)); + TODO_ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'cat', Invalid ## usage when expanding 'cat': Combining 'a' and '\"s\"' yields an invalid token.\n", "", toString(outputList)); +} + +static void hashhash_va_args_unexpected() +{ + const char code[] = + "#define C(...)!##__VA_ARGS__\n" + "C(1)\n"; + simplecpp::OutputList outputList; + preprocess(code, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'C', Invalid ## usage when expanding 'C': Combining '!' and '1' yields an invalid token.\n", toString(outputList)); +} + static void hashhash_universal_character() { const char code[] = - "#define A(x,y) x##y\nint A(\\u01,04);"; + "#define A(x,y) x##y\nint A(\\u01,04);\n"; simplecpp::OutputList outputList; preprocess(code, simplecpp::DUI(), &outputList); ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '\\u01' and '04' yields universal character '\\u0104'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4.\n", toString(outputList)); } +static void hashhash_universal_character_2() +{ + const char code[] = + "#define A(x,y) x##y\nint A(\\U0104, 0104);\n"; + simplecpp::OutputList outputList; + preprocess(code, simplecpp::DUI(), &outputList); + ASSERT_EQUALS("file0,1,syntax_error,failed to expand 'A', Invalid ## usage when expanding 'A': Combining '\\U0104' and '0104' yields universal character '\\U01040104'. This is undefined behavior according to C standard chapter 5.1.1.2, paragraph 4.\n", toString(outputList)); +} + + static void has_include_1() { const char code[] = "#ifdef __has_include\n" @@ -1556,11 +2598,14 @@ static void has_include_1() " #endif\n" "#endif"; simplecpp::DUI dui; - dui.std = "c++17"; - ASSERT_EQUALS("\n\nA", preprocess(code, dui)); + dui.includePaths.emplace_back(testSourceDir); + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); // we default to latest standard internally dui.std = "c++14"; ASSERT_EQUALS("", preprocess(code, dui)); - ASSERT_EQUALS("", preprocess(code)); + dui.std = "c++17"; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); + dui.std = "c++20"; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); } static void has_include_2() @@ -1573,9 +2618,15 @@ static void has_include_2() " #endif\n" "#endif"; simplecpp::DUI dui; + dui.removeComments = true; // TODO: remove this + dui.includePaths.emplace_back(testSourceDir); + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); // we default to latest standard internally + dui.std = "c++14"; + ASSERT_EQUALS("", preprocess(code, dui)); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); - ASSERT_EQUALS("", preprocess(code)); + dui.std = "c++20"; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); } static void has_include_3() @@ -1588,13 +2639,24 @@ static void has_include_3() " #endif\n" "#endif"; simplecpp::DUI dui; - dui.std = "c++17"; + // Test file not found... + ASSERT_EQUALS("\n\n\n\nB", preprocess(code, dui)); // we default to latest standard internally + dui.std = "c++14"; + ASSERT_EQUALS("", preprocess(code, dui)); + dui.std = "c++17"; ASSERT_EQUALS("\n\n\n\nB", preprocess(code, dui)); + // Unless -I is set (preferably, we should differentiate -I and -isystem...) - dui.includePaths.push_back("./testsuite"); + dui.includePaths.emplace_back(testSourceDir + "/testsuite"); + dui.std = ""; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); // we default to latest standard internally + dui.std = "c++14"; + ASSERT_EQUALS("", preprocess(code, dui)); + dui.std = "c++17"; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); + dui.std = "c++20"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); - ASSERT_EQUALS("", preprocess(code)); } static void has_include_4() @@ -1607,9 +2669,14 @@ static void has_include_4() " #endif\n" "#endif"; simplecpp::DUI dui; + dui.includePaths.emplace_back(testSourceDir); // we default to latest standard internally + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); + dui.std = "c++14"; + ASSERT_EQUALS("", preprocess(code, dui)); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); - ASSERT_EQUALS("", preprocess(code)); + dui.std = "c++20"; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); } static void has_include_5() @@ -1622,9 +2689,14 @@ static void has_include_5() " #endif\n" "#endif"; simplecpp::DUI dui; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); // we default to latest standard internally + dui.includePaths.emplace_back(testSourceDir); + dui.std = "c++14"; + ASSERT_EQUALS("", preprocess(code, dui)); dui.std = "c++17"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); - ASSERT_EQUALS("", preprocess(code)); + dui.std = "c++20"; + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); } static void has_include_6() @@ -1637,9 +2709,48 @@ static void has_include_6() " #endif\n" "#endif"; simplecpp::DUI dui; + dui.includePaths.emplace_back(testSourceDir); + ASSERT_EQUALS("\n\nA", preprocess(code, dui)); // we default to latest standard internally + dui.std = "c++99"; + ASSERT_EQUALS("", preprocess(code, dui)); dui.std = "gnu99"; ASSERT_EQUALS("\n\nA", preprocess(code, dui)); - ASSERT_EQUALS("", preprocess(code)); +} + +static void define_has_include_invalid_1() +{ + const char code[] = "#define A)__has_include\n" + "#if\u000BA\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, missing __has_include argument\n", toString(outputList)); +} + +static void define_has_include_invalid_2() +{ + const char code[] = "#define f __has_include\n" + "#if#f<\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, missing __has_include argument\n", toString(outputList)); +} + +static void define_has_include_invalid_3() +{ + const char code[] = "#define\u0000X\u0007__has_include(\n" + "#if%X&\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, sizeof(code)-1, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, invalid __has_include expression\n", toString(outputList)); +} + +static void define_has_include_invalid_4() +{ + const char code[] = "#define\u0000X\u0000__has_include<2\n" + "#if*X\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, sizeof(code)-1, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #if condition, invalid __has_include expression\n", toString(outputList)); } static void strict_ansi_1() @@ -1680,7 +2791,7 @@ static void strict_ansi_4() "#endif"; simplecpp::DUI dui; dui.std = "gnu99"; - dui.defines.push_back("__STRICT_ANSI__"); + dui.defines.emplace_back("__STRICT_ANSI__"); ASSERT_EQUALS("\nA", preprocess(code, dui)); } @@ -1727,10 +2838,64 @@ static void ifA() ASSERT_EQUALS("", preprocess(code)); simplecpp::DUI dui; - dui.defines.push_back("A=1"); + dui.defines.emplace_back("A=1"); ASSERT_EQUALS("\nX", preprocess(code, dui)); } +static void ifFloatingPointLiteral() // #612 +{ + simplecpp::OutputList outputList; + simplecpp::DUI dui; + dui.defines.emplace_back("A=1"); + + const char issue[] = "#if A == 1.0\n" + "int i;\n" + "#endif\n"; + ASSERT_EQUALS("", preprocess(issue, dui, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, floating point literal in preprocessor expression\n", toString(outputList)); + + outputList.clear(); + + const char otherSpelling[] = "#if 0\n" + "#elif .5\n" + "int i;\n" + "#endif\n"; + ASSERT_EQUALS("", preprocess(otherSpelling, &outputList)); + ASSERT_EQUALS("file0,2,syntax_error,failed to evaluate #elif condition, floating point literal in preprocessor expression\n", toString(outputList)); + + outputList.clear(); + + const char exponent[] = "#if 1e3\n" + "int i;\n" + "#endif\n"; + ASSERT_EQUALS("", preprocess(exponent, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, floating point literal in preprocessor expression\n", toString(outputList)); + + outputList.clear(); + + const char suffix[] = "#if 1.f\n" + "int i;\n" + "#endif\n"; + ASSERT_EQUALS("", preprocess(suffix, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, floating point literal in preprocessor expression\n", toString(outputList)); + + outputList.clear(); + + const char hexadecimal[] = "#if 0x1p3\n" + "int i;\n" + "#endif\n"; + ASSERT_EQUALS("", preprocess(hexadecimal, &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, floating point literal in preprocessor expression\n", toString(outputList)); + + outputList.clear(); + + const char integer[] = "#if A == 1\n" + "int i;\n" + "#endif\n"; + ASSERT_EQUALS("\nint i ;", preprocess(integer, dui, &outputList)); + ASSERT_EQUALS("", toString(outputList)); +} + static void ifCharLiteral() { const char code[] = "#if ('A'==0x41)\n" @@ -1746,7 +2911,7 @@ static void ifDefined() "#endif"; simplecpp::DUI dui; ASSERT_EQUALS("", preprocess(code, dui)); - dui.defines.push_back("A=1"); + dui.defines.emplace_back("A=1"); ASSERT_EQUALS("\nX", preprocess(code, dui)); } @@ -1757,7 +2922,7 @@ static void ifDefinedNoPar() "#endif"; simplecpp::DUI dui; ASSERT_EQUALS("", preprocess(code, dui)); - dui.defines.push_back("A=1"); + dui.defines.emplace_back("A=1"); ASSERT_EQUALS("\nX", preprocess(code, dui)); } @@ -1769,7 +2934,7 @@ static void ifDefinedNested() "#endif"; simplecpp::DUI dui; ASSERT_EQUALS("", preprocess(code, dui)); - dui.defines.push_back("FOO=1"); + dui.defines.emplace_back("FOO=1"); ASSERT_EQUALS("\n\nX", preprocess(code, dui)); } @@ -1781,13 +2946,13 @@ static void ifDefinedNestedNoPar() "#endif"; simplecpp::DUI dui; ASSERT_EQUALS("", preprocess(code, dui)); - dui.defines.push_back("FOO=1"); + dui.defines.emplace_back("FOO=1"); ASSERT_EQUALS("\n\nX", preprocess(code, dui)); } static void ifDefinedInvalid1() // #50 - invalid unterminated defined { - const char code[] = "#if defined(A"; + const char code[] = "#if defined(A\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition\n", toString(outputList)); @@ -1795,7 +2960,7 @@ static void ifDefinedInvalid1() // #50 - invalid unterminated defined static void ifDefinedInvalid2() { - const char code[] = "#if defined"; + const char code[] = "#if defined\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition\n", toString(outputList)); @@ -1826,6 +2991,16 @@ static void ifDefinedHashHash2() ASSERT_EQUALS("\n0", preprocess(code, &outputList)); } +static void definedAtEndOfFile() +{ + // "defined" from a macro expansion is undefined behavior; gcc outputs "defined" here. + // The operand is the last token of the file. + const char code[] = "#define X\n" + "#define D defined X\n" + "D"; + ASSERT_EQUALS("\n\n1", preprocess(code)); +} + static void ifLogical() { const char code[] = "#if defined(A) || defined(B)\n" @@ -1834,13 +3009,34 @@ static void ifLogical() simplecpp::DUI dui; ASSERT_EQUALS("", preprocess(code, dui)); dui.defines.clear(); - dui.defines.push_back("A=1"); + dui.defines.emplace_back("A=1"); ASSERT_EQUALS("\nX", preprocess(code, dui)); dui.defines.clear(); - dui.defines.push_back("B=1"); + dui.defines.emplace_back("B=1"); ASSERT_EQUALS("\nX", preprocess(code, dui)); } +static void ifLogicalPrecedence() +{ + // && has higher precedence than || + const char code1[] = "#if 1 || 1 && 0\n" + "X\n" + "#endif"; + ASSERT_EQUALS("\nX", preprocess(code1)); + const char code2[] = "#if 1 or 1 and 0\n" + "X\n" + "#endif"; + ASSERT_EQUALS("\nX", preprocess(code2)); + const char code3[] = "#if 0 && 1 || 1\n" + "X\n" + "#endif"; + ASSERT_EQUALS("\nX", preprocess(code3)); + const char code4[] = "#if 0 || 1 && 0\n" + "X\n" + "#endif"; + ASSERT_EQUALS("", preprocess(code4)); +} + static void ifSizeof() { const char code[] = "#if sizeof(unsigned short)==2\n" @@ -1879,6 +3075,86 @@ static void elif() "3\n" "#endif"; ASSERT_EQUALS("\n\n\n\n\n3", preprocess(code3)); + + // #elif after a taken group must not be evaluated + const char code4[] = "#if 1\n" + "1\n" + "#elif 1/0\n" + "2\n" + "#elif garbage(\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n1", preprocess(code4)); +} + +static void elifdef() +{ + { + const char code[] = "#if 1\n" + "1\n" + "#elifdef X\n" + "2\n" + "#else\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n1", preprocess(code)); + } + { + const char code[] = "#define X\n" + "#if 0\n" + "1\n" + "#elifdef X\n" + "2\n" + "#else\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n\n\n\n2", preprocess(code)); + } + { + const char code[] = "#if 0\n" + "1\n" + "#elifdef X\n" + "2\n" + "#else\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n\n\n\n\n3", preprocess(code)); + } +} + +static void elifndef() +{ + { + const char code[] = "#if 1\n" + "1\n" + "#elifndef X\n" + "2\n" + "#else\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n1", preprocess(code)); + } + { + const char code[] = "#if 0\n" + "1\n" + "#elifndef X\n" + "2\n" + "#else\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n\n\n2", preprocess(code)); + } + { + const char code[] = "#define X\n" + "#if 0\n" + "1\n" + "#elifndef X\n" + "2\n" + "#else\n" + "3\n" + "#endif"; + ASSERT_EQUALS("\n\n\n\n\n\n3", preprocess(code)); + } } static void ifif() @@ -1918,6 +3194,163 @@ static void ifdiv0() ASSERT_EQUALS("", preprocess(code)); } +static void ifShortCircuit() +{ + // operands that are not evaluated must not cause errors + const char * const exprs[] = { + "0 && (1/0)", + "!(1 || (1/0))", + "0 ? 1/0 : 0", + "1 ? 0 : 1/0", + "0 && 1 % 0 && 1", + "(1 || 1/0) && !(0 && 1/0) && 0", + "0 && (2 + (3 * (1/0)))", + "0 && -(1/0)", + "0 and (1/0)", + "!(1 or 1/0)", + "defined(X) and X/0", + "0 && 1 bitor (1/0)", + "!(1 || (1/0) bitand 1)", + "0 and 1 xor (1/0)", + "0 && not (1/0)", + "0 && compl (1/0)", + "!(1 || not not (1/0))" + }; + for (const char * const expr : exprs) { + const std::string code = std::string("#if ") + expr + "\n" + "1\n" + "#else\n" + "2\n" + "#endif\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("\n\n\n2", preprocess(code.c_str(), &outputList)); + ASSERT_EQUALS("", toString(outputList)); + } + + // evaluated operands are still checked + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("#if 1 && (1/0)\n#endif\n", &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, division/modulo by zero\n", toString(outputList)); + outputList.clear(); + ASSERT_EQUALS("", preprocess("#if not (1/0)\n#endif\n", &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, division/modulo by zero\n", toString(outputList)); +} + +static void ifZeroCondition() +{ + // a zero value that is not written as 0 is false + const char * const exprs[] = { + "00 ? 1 : 0", + "-0 ? 1 : 0", + "-X ? 1 : 0", + "0L ? 1 : 0", + "!00 ? 0 : 1", + "!0L ? 0 : 1" + }; + for (const char * const expr : exprs) { + const std::string code = std::string("#if ") + expr + "\n" + "1\n" + "#else\n" + "2\n" + "#endif\n"; + ASSERT_EQUALS("\n\n\n2", preprocess(code.c_str())); + } + + // the selected operand is evaluated + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("#if 00 ? 1 : 1/0\n#endif\n", &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, division/modulo by zero\n", toString(outputList)); +} + +static void ifUnaryOperators() +{ + // unary operators in a row + const char * const exprs[] = { + "!!1", + "!-0", + "!+0", + "!~-1", + "- -1", + "-!0", + "-~0", + "~!0", + "+-1 == -1", + "1 && !-0", + "!-X", + "-(-1) == 1", + "- - -1 == -1", + "1 - -1 == 2" + }; + for (const char * const expr : exprs) { + const std::string code = std::string("#if ") + expr + "\n" + "1\n" + "#else\n" + "2\n" + "#endif\n"; + ASSERT_EQUALS("\n1", preprocess(code.c_str())); + } +} + +static void ifInvalidExpression() +{ + const char * const exprs[] = { + // missing operand + "1 +", + "(1 +)", + "(1 +) + 1", + "* 1", + "(* 1)", + "1 + * 2", + "1 <<", + "(1 ==)", + "1 &", + "(1 &&)", + "|| 1", + "!", + "(!)", + "-", + "~", + "(? 1 : 2)", + "(1 ? 2 :)", + "(1 ? 2 3)", + "(1 ? : 2)", + // missing operator + "1 2", + "(1 2)", + // empty parentheses + "()", + "1 + ()", + // syntax errors are reported also in operands that are not evaluated + "0 && (1 +)" + }; + for (const char * const expr : exprs) { + const std::string code = std::string("#if ") + expr + "\n" + "1\n" + "#endif\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code.c_str(), &outputList)); + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, invalid expression\n", toString(outputList)); + } +} + +static void ifTrueFalse() +{ + const char code[] = "#if true && !false\n" + "1\n" + "#else\n" + "2\n" + "#endif\n"; + simplecpp::DUI dui; + ASSERT_EQUALS("\n1", preprocess(code, dui, nullptr)); + dui.std = "c++11"; + ASSERT_EQUALS("\n1", preprocess(code, dui, nullptr)); + dui.std = "c23"; + ASSERT_EQUALS("\n1", preprocess(code, dui, nullptr)); + // before C23, true and false are ordinary identifiers that evaluate to 0 + dui.std = "c17"; + ASSERT_EQUALS("\n\n\n2", preprocess(code, dui, nullptr)); +} + static void ifalt() // using "and", "or", etc { const char *code; @@ -1937,6 +3370,45 @@ static void ifalt() // using "and", "or", etc ASSERT_EQUALS("\n1", preprocess(code)); } +static void ifAltUnaryOperators() // "not" and "compl" anywhere a unary operator is allowed +{ + const char * const exprs[] = { + // at the start of the expression + "not 0", + "(not 0)", + "not 0 && 1", + "compl 0", + // after a binary operator, "?" or ":" + "1 && not 0", + "1 and not 0", + "0 || not 0", + "1 == not 0", + "0 ? 0 : not 0", + "1 ? not 0 : 0", + // next to another unary operator + "not not 1", + "not -0", + "not ~-1", + "not !0 == 0", + "compl compl 1 == 1", + "compl -1 == 0", + "!(compl -1 + 1 == 0)", + "-compl 0 == 1", + // followed by "(" + "not (0)", + "not(0)", + "compl (0) == -1" + }; + for (const char * const expr : exprs) { + const std::string code = std::string("#if ") + expr + "\n" + "1\n" + "#else\n" + "2\n" + "#endif\n"; + ASSERT_EQUALS("\n1", preprocess(code.c_str())); + } +} + static void ifexpr() { const char code[] = "#define MACRO() (1)\n" @@ -1960,7 +3432,7 @@ static void location1() const char *code; code = "# 1 \"main.c\"\n\n\n" - "x"; + "x"; ASSERT_EQUALS("\n#line 3 \"main.c\"\nx", preprocess(code)); } @@ -2014,6 +3486,91 @@ static void location5() "int x ;", preprocess(code)); } +static void location6() +{ + const char code[] = + "#line 3\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "\n" + "3 \"\"", + preprocess(code)); +} + +static void location7() +{ + const char code[] = + "#line 3 \"file.c\"\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "#line 3 \"file.c\"\n" + "3 \"file.c\"", + preprocess(code)); +} + +static void location8() +{ + const char code[] = + "# 3\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "\n" + "3 \"\"", + preprocess(code)); +} + +static void location9() +{ + const char code[] = + "# 3 \"file.c\"\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "#line 3 \"file.c\"\n" + "3 \"file.c\"", + preprocess(code)); +} + +static void location10() +{ + const char code[] = + "#line 3\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "\n" // TODO: should this have the #line marker? + "3 \"\"", + preprocess(code)); +} + +static void location11() +{ + const char code[] = + "#line 3 \"file.c\"\n" + "__LINE__ __FILE__\n" + "#line 33 \"file2.c\"\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "#line 3 \"file.c\"\n" + "3 \"file.c\"\n" + "#line 33 \"file2.c\"\n" + "33 \"file2.c\"", + preprocess(code)); +} + +static void location12() +{ + simplecpp::DUI dui; + dui.removeComments = true; + + const char code[] = + "/**//**/#/**//**/line/**//**/3/**//**/\"file.c\"/**/\n" + "__LINE__ __FILE__\n"; + ASSERT_EQUALS("\n" + "#line 3 \"file.c\"\n" + "3 \"file.c\"", + preprocess(code, dui)); +} + + static void missingHeader1() { const char code[] = "#include \"notexist.h\"\n"; @@ -2032,7 +3589,7 @@ static void missingHeader2() simplecpp::TokenList tokens2(files); const simplecpp::TokenList rawtokens = makeTokenList(code,files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("", toString(outputList)); } @@ -2049,7 +3606,9 @@ static void missingHeader4() { const char code[] = "#/**/include <>\n"; simplecpp::OutputList outputList; - ASSERT_EQUALS("", preprocess(code, &outputList)); + simplecpp::DUI dui; + dui.removeComments = true; // TODO: remove this + ASSERT_EQUALS("", preprocess(code, dui, &outputList)); ASSERT_EQUALS("file0,1,syntax_error,No header in #include\n", toString(outputList)); } @@ -2064,7 +3623,7 @@ static void nestedInclude() simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("file0,1,include_nested_too_deeply,#include nested too deeply\n", toString(outputList)); @@ -2082,7 +3641,7 @@ static void systemInclude() simplecpp::OutputList outputList; simplecpp::TokenList tokens2(files); simplecpp::DUI dui; - dui.includePaths.push_back("include"); + dui.includePaths.emplace_back("include"); simplecpp::preprocess(tokens2, rawtokens, files, cache, dui, &outputList); ASSERT_EQUALS("", toString(outputList)); @@ -2261,18 +3820,233 @@ static void nullDirective2() "#endif\n" "x = a;\n"; - ASSERT_EQUALS("\n\n\n\nx = 1 ;", preprocess(code)); -} + ASSERT_EQUALS("\n\n\n\nx = 1 ;", preprocess(code)); +} + +static void nullDirective3() +{ + const char code[] = "#if 1\n" + "#define a 1\n" + "#\n" + "#endif\n" + "x = a;\n"; + + ASSERT_EQUALS("\n\n\n\nx = 1 ;", preprocess(code)); +} + +static void lineDirective() +{ + for (const std::string std : {"c89", "c90", "c++03"}) { + std::string std_name; + if (simplecpp::getCStd(std) != simplecpp::CUnknown) + std_name = simplecpp::getCStdName(simplecpp::getCStd(std)); + else + std_name = simplecpp::getCppStdName(simplecpp::getCppStd(std)); + + simplecpp::DUI dui; + dui.std = std; + + { + const char code[] = + "#line -1\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,syntax_error,Invalid character in line directive: '-'.\n", + toString(outputList)); + } + + { + const char code[] = + "#line 0\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 0. Line number zero is undefined behavior.\n", + toString(outputList)); + } + + { + const char code[] = + "#line 32767\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + + { + const char code[] = + "#line 32768\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 32768. Line numbers above 32767 are undefined behavior in " + std_name + ".\n", + toString(outputList)); + } + + { + const char code[] = + "#line 18446744073709551617\n" // 2^64 + 1 + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 18446744073709551617. Line numbers above 32767 are undefined behavior in " + std_name + ".\n", + toString(outputList)); + } + } + + for (const std::string std : {"c99", "c11", "c17", "c23", "c2y", "c++11", "c++14", "c++17", "c++20", "c++23"}) { + std::string std_name; + if (simplecpp::getCStd(std) != simplecpp::CUnknown) + std_name = simplecpp::getCStdName(simplecpp::getCStd(std)); + else + std_name = simplecpp::getCppStdName(simplecpp::getCppStd(std)); + + simplecpp::DUI dui; + dui.std = std; + + { + const char code[] = + "#line -1\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,syntax_error,Invalid character in line directive: '-'.\n", + toString(outputList)); + } + + { + const char code[] = + "#line 0\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 0. Line number zero is undefined behavior.\n", + toString(outputList)); + } + + { + const char code[] = + "#line 32767\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + + { + const char code[] = + "#line 32768\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + + { + const char code[] = + "#line 2147483647\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + + { + const char code[] = + "#line 2147483648\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 2147483648. Line numbers above 2147483647 are undefined behavior in " + std_name + ".\n", + toString(outputList)); + } + + { + const char code[] = + "#line 18446744073709551617\n" // 2^64 + 1 + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 18446744073709551617. Line numbers above 2147483647 are undefined behavior in " + std_name + ".\n", + toString(outputList)); + } + } + + { + const std::string std_name = "C++26"; + + simplecpp::DUI dui; + dui.std = "c++26"; + + { + const char code[] = + "#line -1\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,syntax_error,Invalid character in line directive: '-'.\n", + toString(outputList)); + } + + { + const char code[] = + "#line 0\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 0. Line number zero is undefined behavior.\n", + toString(outputList)); + } + + { + const char code[] = + "#line 32767\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + + { + const char code[] = + "#line 32768\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } -static void nullDirective3() -{ - const char code[] = "#if 1\n" - "#define a 1\n" - "#\n" - "#endif\n" - "x = a;\n"; + { + const char code[] = + "#line 2147483647\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } - ASSERT_EQUALS("\n\n\n\nx = 1 ;", preprocess(code)); + { + const char code[] = + "#line 2147483648\n" + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 2147483648. Line numbers above 2147483647 are conditionally supported in " + std_name + ".\n", + toString(outputList)); + } + + { + const char code[] = + "#line 18446744073709551617\n" // 2^64 + 1 + ";\n"; + simplecpp::OutputList outputList; + makeTokenList(code, dui, &outputList); + ASSERT_EQUALS("file0,2,portability_line_directive,Line number out of range: 18446744073709551617. Line numbers above 2147483647 are conditionally supported in " + std_name + ".\n", + toString(outputList)); + } + } } static void include1() @@ -2290,7 +4064,7 @@ static void include2() static void include3() // #16 - crash when expanding macro from header { const char code_c[] = "#include \"A.h\"\n" - "glue(1,2,3,4)\n" ; + "glue(1,2,3,4)\n"; const char code_h[] = "#define glue(a,b,c,d) a##b##c##d\n"; std::vector files; @@ -2308,7 +4082,7 @@ static void include3() // #16 - crash when expanding macro from header simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n1234", out.stringify()); @@ -2317,7 +4091,7 @@ static void include3() // #16 - crash when expanding macro from header static void include4() // #27 - -include { - const char code_c[] = "X\n" ; + const char code_c[] = "X\n"; const char code_h[] = "#define X 123\n"; std::vector files; @@ -2335,8 +4109,8 @@ static void include4() // #27 - -include simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); - dui.includes.push_back("27.h"); + dui.includePaths.emplace_back("."); + dui.includes.emplace_back("27.h"); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("123", out.stringify()); @@ -2362,7 +4136,7 @@ static void include5() // #3 - handle #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"3.h\"\n123", out.stringify()); @@ -2408,7 +4182,7 @@ static void include7() // #include MACRO simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"3.h\"\n123", out.stringify()); @@ -2446,17 +4220,76 @@ static void include9() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); + dui.removeComments = true; simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 2 \"1.h\"\nx = 1 ;", out.stringify()); } +static void include10() // #669 - -include with load() +{ + const char code_c[] = "X\n"; + const char code_h[] = "#define X 123\n"; + + std::vector files; + + const simplecpp::TokenList rawtokens_c = makeTokenList(code_c, files, "src.c"); + const simplecpp::TokenList rawtokens_h = makeTokenList(code_h, files, "inc.h"); + + ASSERT_EQUALS(2U, files.size()); + ASSERT_EQUALS("src.c", files[0]); + ASSERT_EQUALS("inc.h", files[1]); + + simplecpp::FileDataCache cache; + cache.insert({"src.c", rawtokens_c}); + cache.insert({"inc.h", rawtokens_h}); + + simplecpp::OutputList outputList; + simplecpp::DUI dui; + dui.includes.emplace_back("inc.h"); + dui.includes.emplace_back("missing.h"); + cache = simplecpp::load(rawtokens_c, files, dui, &outputList, std::move(cache)); + + ASSERT_EQUALS(1, outputList.size()); + ASSERT_EQUALS("Can not open include file 'missing.h' that is explicitly included.", outputList.begin()->msg); +} + +static void include11() // #669 - -include with preprocess() +{ + const char code_c[] = "X\n"; + const char code_h[] = "#define X 123\n"; + + std::vector files; + + const simplecpp::TokenList rawtokens_c = makeTokenList(code_c, files, "src.c"); + const simplecpp::TokenList rawtokens_h = makeTokenList(code_h, files, "inc.h"); + + ASSERT_EQUALS(2U, files.size()); + ASSERT_EQUALS("src.c", files[0]); + ASSERT_EQUALS("inc.h", files[1]); + + simplecpp::FileDataCache cache; + cache.insert({"src.c", rawtokens_c}); + cache.insert({"inc.h", rawtokens_h}); + + simplecpp::OutputList outputList; + simplecpp::TokenList out(files); + simplecpp::DUI dui; + dui.includes.emplace_back("inc.h"); + dui.includes.emplace_back("missing.h"); + simplecpp::preprocess(out, rawtokens_c, files, cache, dui, &outputList); + + ASSERT_EQUALS("123", out.stringify()); + ASSERT_EQUALS(1, outputList.size()); + ASSERT_EQUALS("Can not open include file 'missing.h' that is explicitly included.", outputList.begin()->msg); +} + static void readfile_nullbyte() { - const char code[] = "ab\0cd"; + const char code[] = "ab\0cd\n"; simplecpp::OutputList outputList; - ASSERT_EQUALS("ab cd", readfile(code,sizeof(code), &outputList)); + ASSERT_EQUALS("ab cd", readfile(code,sizeof(code)-1,{},&outputList)); ASSERT_EQUALS(true, outputList.empty()); // should warning be written? } @@ -2484,11 +4317,16 @@ static void readfile_char_error() { simplecpp::OutputList outputList; - readfile("A = L's", &outputList); + readfile("A = L's", {}, &outputList); + ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\'). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); + outputList.clear(); + + readfile("A = 's\n'", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\'). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); outputList.clear(); - readfile("A = 's\n'", &outputList); + // the ' at the end of the file is not dropped after the peek for a digit separator + readfile("A = 1'", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\'). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); } @@ -2542,36 +4380,36 @@ static void readfile_string_error() { simplecpp::OutputList outputList; - readfile("A = \"abs", &outputList); + readfile("A = \"abs", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\"). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); outputList.clear(); - readfile("A = u8\"abs\n\"", &outputList); + readfile("A = u8\"abs\n\"", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\"). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); outputList.clear(); - readfile("A = R\"as\n(abc)as\"", &outputList); + readfile("A = R\"as\n(abc)as\"", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,Invalid newline in raw string delimiter.\n", toString(outputList)); outputList.clear(); - readfile("A = u8R\"as\n(abc)as\"", &outputList); + readfile("A = u8R\"as\n(abc)as\"", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,Invalid newline in raw string delimiter.\n", toString(outputList)); outputList.clear(); - readfile("A = R\"as(abc)a\"", &outputList); + readfile("A = R\"as(abc)a\"", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,Raw string missing terminating delimiter.\n", toString(outputList)); outputList.clear(); - readfile("A = LR\"as(abc)a\"", &outputList); + readfile("A = LR\"as(abc)a\"", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,Raw string missing terminating delimiter.\n", toString(outputList)); outputList.clear(); - readfile("#define A \"abs", &outputList); + readfile("#define A \"abs", {}, &outputList); ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\"). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); outputList.clear(); // Don't warn for a multiline define - readfile("#define A \"abs\\\n\"", &outputList); + readfile("#define A \"abs\\\n\"\n", {}, &outputList); ASSERT_EQUALS("", toString(outputList)); } @@ -2583,11 +4421,11 @@ static void readfile_cpp14_number() static void readfile_unhandled_chars() { simplecpp::OutputList outputList; - readfile("// 你好世界", &outputList); + readfile("// 你好世界\n", {}, &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("s=\"你好世界\"", &outputList); + readfile("s=\"你好世界\"\n", {}, &outputList); ASSERT_EQUALS("", toString(outputList)); - readfile("int 你好世界=0;", &outputList); + readfile("int 你好世界=0;\n", {}, &outputList); ASSERT_EQUALS("file0,1,unhandled_char_error,The code contains unhandled character(s) (character code=228). Neither unicode nor extended ascii is supported.\n", toString(outputList)); } @@ -2603,8 +4441,146 @@ static void readfile_file_not_found() { simplecpp::OutputList outputList; std::vector files; - (void)simplecpp::TokenList("NotAFile", files, &outputList); - ASSERT_EQUALS("file0,1,file_not_found,File is missing: NotAFile\n", toString(outputList)); + (void)simplecpp::TokenList("NotAFile", files, {}, &outputList); + ASSERT_EQUALS("file0,0,file_not_found,File is missing: NotAFile\n", toString(outputList)); +} + +static void readfile_no_eof_newline() +{ + { + const char code[] = ""; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "\\\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); + } + { + const char code[] = "\\\n"; + simplecpp::DUI dui; + dui.std = "c++03"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, dui, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "#define A"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); + } + { + const char code[] = "#define A\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "#define A\\"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); + } + { + const char code[] = "// comment"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); + } + { + const char code[] = "// comment\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "/* comment \n comment */"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("file0,2,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); + } + { + const char code[] = "/* comment \n comment */\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + // CR is a newline + const char code[] = "x\r"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "x\r\n"; + simplecpp::OutputList outputList; + readfile(code, sizeof(code)-1, {}, &outputList); + ASSERT_EQUALS("", toString(outputList)); + } +} + +static void readfile_cr_newlines() +{ + // CR, CR LF and LF are all newlines + ASSERT_EQUALS("a\nb\nc", readfile("a\rb\rc\r")); + ASSERT_EQUALS("a\nb\nc", readfile("a\r\nb\r\nc\r\n")); + ASSERT_EQUALS("a\nb\nc\n\n\nd", readfile("a\rb\r\nc\n\r\rd\n")); + ASSERT_EQUALS("# define A 1\nA", readfile("#define A 1\rA\r")); +} + +static void readfile_filestream() +{ + // the edge cases from the other readfile tests, read from a file + { + const char code[] = "a\rb\rc\r"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("a\nb\nc", readfileFromFile(code, sizeof(code)-1, &outputList)); + ASSERT_EQUALS("", toString(outputList)); + } + { + const char code[] = "a\r\nb\r\nc\r\n"; + ASSERT_EQUALS("a\nb\nc", readfileFromFile(code, sizeof(code)-1)); + } + { + const char code[] = "a\rb\r\nc\n\r\rd\n"; + ASSERT_EQUALS("a\nb\nc\n\n\nd", readfileFromFile(code, sizeof(code)-1)); + } + { + const char code[] = "1'"; + simplecpp::OutputList outputList; + (void)readfileFromFile(code, sizeof(code)-1, &outputList); + ASSERT_EQUALS("file0,1,syntax_error,No pair for character (\'). Can't process file. File is either invalid or unicode, which is currently not supported.\n", toString(outputList)); + } + { + // UTF-16LE with CR newlines + const char code[] = "\xff\xfe\x78\x00\x0d\x00\x79\x00\x0d\x00"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("x\ny", readfileFromFile(code, sizeof(code)-1, &outputList)); + ASSERT_EQUALS("", toString(outputList)); + } + { + // UTF-16BE with CR newlines + const char code[] = "\xfe\xff\x00\x78\x00\x0d\x00\x79\x00\x0d"; + ASSERT_EQUALS("x\ny", readfileFromFile(code, sizeof(code)-1)); + } + { + // UTF-16 with an odd number of bytes, the dangling last byte is dropped + const char code[] = "\xff\xfe\x31\x00\x0a"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("1", readfileFromFile(code, sizeof(code)-1, &outputList)); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); + } } static void stringify1() @@ -2628,42 +4604,25 @@ static void stringify1() simplecpp::TokenList out(files); simplecpp::DUI dui; - dui.includePaths.push_back("."); + dui.includePaths.emplace_back("."); simplecpp::preprocess(out, rawtokens_c, files, cache, dui); ASSERT_EQUALS("\n#line 1 \"A.h\"\n1\n2\n#line 1 \"A.h\"\n1\n2", out.stringify()); } +// Token::macro, the locations of expanded tokens and the macro usage are used by cppcheck static void tokenMacro1() { const char code[] = "#define A 123\n" "A"; - std::vector files; - simplecpp::FileDataCache cache; - simplecpp::TokenList tokenList(files); - const simplecpp::TokenList rawtokens = makeTokenList(code,files); - simplecpp::preprocess(tokenList, rawtokens, files, cache, simplecpp::DUI()); - ASSERT_EQUALS("A", tokenList.cback()->macro); + ASSERT_EQUALS("123@A(2:1)", expandedTokens(code)); } static void tokenMacro2() { const char code[] = "#define ADD(X,Y) X+Y\n" "ADD(1,2)"; - std::vector files; - simplecpp::FileDataCache cache; - simplecpp::TokenList tokenList(files); - const simplecpp::TokenList rawtokens = makeTokenList(code,files); - simplecpp::preprocess(tokenList, rawtokens, files, cache, simplecpp::DUI()); - const simplecpp::Token *tok = tokenList.cfront(); - ASSERT_EQUALS("1", tok->str()); - ASSERT_EQUALS("", tok->macro); - tok = tok->next; - ASSERT_EQUALS("+", tok->str()); - ASSERT_EQUALS("ADD", tok->macro); - tok = tok->next; - ASSERT_EQUALS("2", tok->str()); - ASSERT_EQUALS("", tok->macro); + ASSERT_EQUALS("1(2:1) +@ADD(2:1) 2(2:1)", expandedTokens(code)); } static void tokenMacro3() @@ -2671,20 +4630,9 @@ static void tokenMacro3() const char code[] = "#define ADD(X,Y) X+Y\n" "#define FRED 1\n" "ADD(FRED,2)"; - std::vector files; - simplecpp::FileDataCache cache; - simplecpp::TokenList tokenList(files); - const simplecpp::TokenList rawtokens = makeTokenList(code,files); - simplecpp::preprocess(tokenList, rawtokens, files, cache, simplecpp::DUI()); - const simplecpp::Token *tok = tokenList.cfront(); - ASSERT_EQUALS("1", tok->str()); - ASSERT_EQUALS("FRED", tok->macro); - tok = tok->next; - ASSERT_EQUALS("+", tok->str()); - ASSERT_EQUALS("ADD", tok->macro); - tok = tok->next; - ASSERT_EQUALS("2", tok->str()); - ASSERT_EQUALS("", tok->macro); + std::list macroUsage; + ASSERT_EQUALS("1@FRED(3:1) +@ADD(3:1) 2(3:1)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("ADD 1:9 use 3:1\nFRED 2:9 use 3:1\n", toString(macroUsage)); } static void tokenMacro4() @@ -2692,14 +4640,9 @@ static void tokenMacro4() const char code[] = "#define A B\n" "#define B 1\n" "A"; - std::vector files; - simplecpp::FileDataCache cache; - simplecpp::TokenList tokenList(files); - const simplecpp::TokenList rawtokens = makeTokenList(code,files); - simplecpp::preprocess(tokenList, rawtokens, files, cache, simplecpp::DUI()); - const simplecpp::Token * const tok = tokenList.cfront(); - ASSERT_EQUALS("1", tok->str()); - ASSERT_EQUALS("A", tok->macro); + std::list macroUsage; + ASSERT_EQUALS("1@A(3:1)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("A 1:9 use 3:1\nB 2:9 use 3:1\n", toString(macroUsage)); } static void tokenMacro5() @@ -2707,14 +4650,111 @@ static void tokenMacro5() const char code[] = "#define SET_BPF(code) (code)\n" "#define SET_BPF_JUMP(code) SET_BPF(D | code)\n" "SET_BPF_JUMP(A | B | C);"; - std::vector files; - simplecpp::FileDataCache cache; - simplecpp::TokenList tokenList(files); - const simplecpp::TokenList rawtokens = makeTokenList(code,files); - simplecpp::preprocess(tokenList, rawtokens, files, cache, simplecpp::DUI()); - const simplecpp::Token * const tok = tokenList.cfront()->next; - ASSERT_EQUALS("D", tok->str()); - ASSERT_EQUALS("SET_BPF_JUMP", tok->macro); + ASSERT_EQUALS("(@SET_BPF(3:1) D@SET_BPF_JUMP(3:1) |@SET_BPF_JUMP(3:1) A(3:1) |(3:1) B(3:1) |(3:1) C(3:1) )@SET_BPF(3:1) ;(3:24)", expandedTokens(code)); +} + +static void tokenMacro6() +{ + const char code[] = "#define FILE() __FILE__\n" + "#define LINE() __LINE__\n" + "#define COUNTER() __COUNTER__\n" + "FILE()\n" + "LINE()\n" + "COUNTER()\n"; + ASSERT_EQUALS("\"\"@__FILE__(4:1) 5@__LINE__(5:1) 0@__COUNTER__(6:1)", expandedTokens(code)); +} + +static void tokenMacro7() +{ + const char code[] = "#define A 1+2\n" + "x A y\n"; + std::list macroUsage; + ASSERT_EQUALS("x(2:1) 1@A(2:3) +@A(2:3) 2@A(2:3) y(2:5)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("A 1:9 use 2:3\n", toString(macroUsage)); +} + +static void tokenMacro8() +{ + // the call g(2) is completed with tokens after the expansion of f. + // The tokens get the location of the ( and the macro g + const char code[] = "#define f g\n" + "#define g(x) x+1\n" + "f(2)\n"; + std::list macroUsage; + ASSERT_EQUALS("2(3:2) +@g(3:2) 1@g(3:2)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("f 1:9 use 3:1\ng 2:9 use 3:2\n", toString(macroUsage)); +} + +static void tokenMacro9() +{ + // the arguments of g( come after the expansion of f + const char code[] = "#define g(x) x+1\n" + "#define f g(\n" + "f 2)\n"; + std::list macroUsage; + ASSERT_EQUALS("2(3:3) +@g(3:3) 1@g(3:3)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("f 2:9 use 3:1\ng 1:9 use 3:3\n", toString(macroUsage)); +} + +static void tokenMacro10() +{ + // the results of # and ## have no macro name + const char code[] = "#define str(x) #x\n" + "#define cat(a,b) a##b\n" + "str(a b) cat(x,y)\n"; + std::list macroUsage; + ASSERT_EQUALS("\"a b\"(3:1) xy(3:10)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("cat 2:9 use 3:10\nstr 1:9 use 3:1\n", toString(macroUsage)); +} + +static void tokenMacro11() +{ + const char code[] = "#define F(...) f(__VA_ARGS__)\n" + "F(1,2)\n"; + std::list macroUsage; + ASSERT_EQUALS("f@F(2:1) (@F(2:1) 1(2:1) ,(2:1) 2(2:1) )@F(2:1)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("F 1:9 use 2:1\n", toString(macroUsage)); +} + +static void tokenMacro12() +{ + const char code[] = "#define f f\n" + "f\n"; + std::list macroUsage; + ASSERT_EQUALS("f@f(2:1)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("f 1:9 use 2:1\n", toString(macroUsage)); +} + +static void tokenMacro13() +{ + // all tokens of the expansion get the location of the macro name + const char code[] = "#define ADD(X,Y) X+Y\n" + "ADD(1,\n" + " 2)\n" + "z\n"; + std::list macroUsage; + ASSERT_EQUALS("1(2:1) +@ADD(2:1) 2(2:1) z(4:1)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("ADD 1:9 use 2:1\n", toString(macroUsage)); +} + +static void tokenMacro14() +{ + // A is used in the argument and in the body, so it is listed twice + const char code[] = "#define A 1\n" + "#define B(x) x A\n" + "B(A)\n"; + std::list macroUsage; + ASSERT_EQUALS("1@A(3:1) 1@A(3:1)", expandedTokens(code, simplecpp::DUI(), ¯oUsage)); + ASSERT_EQUALS("A 1:9 use 3:1\nA 1:9 use 3:1\nB 2:9 use 3:1\n", toString(macroUsage)); +} + +static void tokenMacro15() +{ + simplecpp::DUI dui; + dui.defines.emplace_back("D=1+2"); + std::list macroUsage; + ASSERT_EQUALS("x(1:1) 1@D(1:3) +@D(1:3) 2@D(1:3)", expandedTokens("x D\n", dui, ¯oUsage)); + ASSERT_EQUALS("D 1:1 use 1:3 unknown\n", toString(macroUsage)); } static void undef() @@ -2731,7 +4771,7 @@ static void userdef() { const char code[] = "#ifdef A\n123\n#endif\n"; simplecpp::DUI dui; - dui.defines.push_back("A=1"); + dui.defines.emplace_back("A=1"); ASSERT_EQUALS("\n123", preprocess(code, dui)); } @@ -2750,31 +4790,43 @@ static void unicode() { { const char code[] = "\xFE\xFF\x00\x31\x00\x32"; - ASSERT_EQUALS("12", readfile(code, sizeof(code))); + ASSERT_EQUALS("12", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x31\x00\x32\x00"; - ASSERT_EQUALS("12", readfile(code, sizeof(code))); + ASSERT_EQUALS("12", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE\xFF\x00\x2f\x00\x2f\x00\x0a\x00\x31"; - ASSERT_EQUALS("//\n1", readfile(code, sizeof(code))); + ASSERT_EQUALS("//\n1", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x2f\x00\x2f\x00\x0a\x00\x31\x00"; - ASSERT_EQUALS("//\n1", readfile(code, sizeof(code))); + ASSERT_EQUALS("//\n1", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE\xFF\x00\x22\x00\x61\x00\x22"; - ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code))); + ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x22\x00\x61\x00\x22\x00"; - ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code))); + ASSERT_EQUALS("\"a\"", readfile(code, sizeof(code)-1)); } { const char code[] = "\xff\xfe\x0d\x00\x0a\x00\x2f\x00\x2f\x00\x31\x00\x0d\x00\x0a\x00"; - ASSERT_EQUALS("\n//1", readfile(code, sizeof(code))); + ASSERT_EQUALS("\n//1", readfile(code, sizeof(code)-1)); + } + { + // CR newlines + const char code[] = "\xff\xfe\x78\x00\x0d\x00\x79\x00\x0d\x00"; + ASSERT_EQUALS("x\ny", readfile(code, sizeof(code)-1)); + } + { + // odd number of bytes, the dangling last byte is dropped + const char code[] = "\xff\xfe\x31\x00\x0a"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("1", readfile(code, sizeof(code)-1, {}, &outputList)); + ASSERT_EQUALS("file0,1,portability_no_eof_newline,No newline at end of file is undefined behavior in C.\n", toString(outputList)); } } @@ -2782,41 +4834,41 @@ static void unicode_invalid() { { const char code[] = "\xFF"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFE\xFF\x31"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + const char code[] = "\xFE\xFF\x31\x00"; + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFF\xFE\x31"; - ASSERT_EQUALS("1", readfile(code, sizeof(code))); + const char code[] = "\xFF\xFE\x31\x00"; + ASSERT_EQUALS("1", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFE\xFF\x31\x32"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { const char code[] = "\xFF\xFE\x31\x32"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFE\xFF\x00\x31\x00\x32\x33"; - ASSERT_EQUALS("", readfile(code, sizeof(code))); + const char code[] = "\xFE\xFF\x00\x31\x00\x32\x33\x00"; + ASSERT_EQUALS("", readfile(code, sizeof(code)-1)); } { - const char code[] = "\xFF\xFE\x31\x00\x32\x00\x33"; - ASSERT_EQUALS("123", readfile(code, sizeof(code))); + const char code[] = "\xFF\xFE\x31\x00\x32\x00\x33\x00"; + ASSERT_EQUALS("123", readfile(code, sizeof(code)-1)); } } static void warning() { - const char code[] = "#warning MSG\n1"; + const char code[] = "#warning MSG\n1\n"; simplecpp::OutputList outputList; ASSERT_EQUALS("\n1", preprocess(code, &outputList)); ASSERT_EQUALS("file0,1,#warning,#warning MSG\n", toString(outputList)); @@ -2912,17 +4964,17 @@ static void preprocessSizeOf() { simplecpp::OutputList outputList; - ASSERT_EQUALS("", preprocess("#if 3 > sizeof", &outputList)); + ASSERT_EQUALS("", preprocess("#if 3 > sizeof\n", &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, missing sizeof argument\n", toString(outputList)); outputList.clear(); - ASSERT_EQUALS("", preprocess("#if 3 > sizeof A", &outputList)); + ASSERT_EQUALS("", preprocess("#if 3 > sizeof A\n", &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, missing sizeof argument\n", toString(outputList)); outputList.clear(); - ASSERT_EQUALS("", preprocess("#if 3 > sizeof(int", &outputList)); + ASSERT_EQUALS("", preprocess("#if 3 > sizeof(int\n", &outputList)); ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition, invalid sizeof expression\n", toString(outputList)); } @@ -2973,6 +5025,7 @@ static void stdcVersionDefine() " __STDC_VERSION__\n" "#endif\n"; simplecpp::DUI dui; + ASSERT_EQUALS("", preprocess(code, dui)); dui.std = "c11"; ASSERT_EQUALS("\n201112L", preprocess(code, dui)); } @@ -2983,6 +5036,7 @@ static void cpluscplusDefine() " __cplusplus\n" "#endif\n"; simplecpp::DUI dui; + ASSERT_EQUALS("", preprocess(code, dui)); dui.std = "c++11"; ASSERT_EQUALS("\n201103L", preprocess(code, dui)); } @@ -3076,7 +5130,7 @@ static void stdValid() static void assertToken(const std::string& s, bool name, bool number, bool comment, char op, int line) { const std::vector f; - const simplecpp::Location l(f); + const simplecpp::Location l; const simplecpp::Token t(s, l); assertEquals(name, t.name, line); assertEquals(number, t.number, line); @@ -3169,6 +5223,275 @@ static void preprocess_files() } } +static void tokenlist_api() +{ + std::vector filenames; +# if !defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) + // sized array + size + { + char input[] = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); + } + { + const char input[] = "code"; + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); + } + { + unsigned char input[] = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); + } + { + const unsigned char input[] = "code"; + simplecpp::TokenList(input,sizeof(input)-1,filenames,""); + } +#endif // !defined(__cpp_lib_string_view) && !defined(__cpp_lib_span) + // pointer via View + { + const char * const input = "code"; + simplecpp::TokenList({input},filenames,""); + } + // sized array via View + { + char input[] = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList(simplecpp::View{input},filenames,""); + } + { + const char input[] = "code"; + simplecpp::TokenList(simplecpp::View{input},filenames,""); + } + // sized array + size via View/std::span + { + char input[] = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList({input,sizeof(input)-1},filenames,""); + } + { + const char input[] = "code"; + simplecpp::TokenList({input,sizeof(input)-1},filenames,""); + } + // sized array + { + char input[] = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList(input,filenames,""); + } + { + const char input[] = "code"; + simplecpp::TokenList(input,filenames,""); + } + { + unsigned char input[] = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList(input,filenames,""); + } + { + const unsigned char input[] = "code"; + simplecpp::TokenList(input,filenames,""); + } + // std::string via View/std::span (implicit) + { + std::string input = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList(input,filenames,""); + } + { + const std::string input = "code"; + simplecpp::TokenList(input,filenames,""); + } + // std::string via View/std::span (explicit) + { + std::string input = "code"; // NOLINT(misc-const-correctness) + simplecpp::TokenList({input},filenames,""); + } + { + const std::string input = "code"; + simplecpp::TokenList({input},filenames,""); + } + + // this test is to make sure the safe APIs are compiling +#ifdef __cpp_lib_string_view + { + const char input[] = "code"; + const std::string_view sv = input; + // std::string_view can be implicitly converted into a std::span + simplecpp::TokenList(sv,filenames,""); + } +#endif // __cpp_lib_string_view +#ifdef __cpp_lib_span + { + char input[] = "code"; + const std::span sp = input; + simplecpp::TokenList(sp,filenames,""); + } + { + const char input[] = "code"; + const std::span sp = input; + simplecpp::TokenList(sp,filenames,""); + } + { + unsigned char input[] = "code"; + const std::span sp = input; + simplecpp::TokenList(sp,filenames,""); + } + { + const unsigned char input[] = "code"; + const std::span sp = input; + simplecpp::TokenList(sp,filenames,""); + } +#endif // __cpp_lib_span +} + +static void bad_macro_syntax() // #616 +{ + simplecpp::DUI dui; + dui.defines.emplace_back("\""); + + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess("", dui, &outputList)); + ASSERT_EQUALS(1, outputList.size()); + ASSERT_EQUALS(simplecpp::Output::Type::DUI_ERROR, outputList.cbegin()->type); + ASSERT_EQUALS("bad macro syntax. macroname=\" value=1", outputList.cbegin()->msg); +} + +static void ifCond() +{ + { + const char code[] = "int i;"; + std::list ifCond; + ASSERT_EQUALS("int i ;", preprocess(code, &ifCond)); + ASSERT_EQUALS(0, ifCond.size()); + } + { + const char code[] = "#if 0\n" + "# elif __GNUC__ == 1\n" + "# elif defined(__APPLE__)\n" + "#endif\n"; + std::list ifCond; + ASSERT_EQUALS("", preprocess(code, &ifCond)); + ASSERT_EQUALS(3, ifCond.size()); + auto it = ifCond.cbegin(); + ASSERT_EQUALS(0, it->location.fileIndex); + ASSERT_EQUALS(1, it->location.line); + ASSERT_EQUALS(2, it->location.col); + ASSERT_EQUALS("0", it->E); + ASSERT_EQUALS(0, it->result); + ++it; + ASSERT_EQUALS(0, it->location.fileIndex); + ASSERT_EQUALS(2, it->location.line); + ASSERT_EQUALS(3, it->location.col); + ASSERT_EQUALS("__GNUC__ == 1", it->E); + ASSERT_EQUALS(0, it->result); + ++it; + ASSERT_EQUALS(0, it->location.fileIndex); + ASSERT_EQUALS(3, it->location.line); + ASSERT_EQUALS(4, it->location.col); + ASSERT_EQUALS("0", it->E); + ASSERT_EQUALS(0, it->result); + } + { + const char code[] = "#ifdef NOTDEFINED\n" + "#endif\n" + "#ifndef NOTDEFINED\n" + "#endif\n"; + std::list ifCond; + ASSERT_EQUALS("", preprocess(code, &ifCond)); + ASSERT_EQUALS(2, ifCond.size()); + auto it = ifCond.cbegin(); + ASSERT_EQUALS(0, it->location.fileIndex); + ASSERT_EQUALS(1, it->location.line); + ASSERT_EQUALS(2, it->location.col); + ASSERT_EQUALS("0", it->E); + ASSERT_EQUALS(0, it->result); + ++it; + ASSERT_EQUALS(0, it->location.fileIndex); + ASSERT_EQUALS(3, it->location.line); + ASSERT_EQUALS(2, it->location.col); + ASSERT_EQUALS("1", it->E); + ASSERT_EQUALS(1, it->result); + } +} + +static void macroUsage() +{ + { + const char code[] = "int i;"; + std::list macroUsage; + ASSERT_EQUALS("int i ;", preprocess(code, ¯oUsage)); + ASSERT_EQUALS(0, macroUsage.size()); + } + { + const char code[] = "#define DEF_1\n" + "#ifdef DEF_1\n" + "#endif\n"; + std::list macroUsage; + ASSERT_EQUALS("", preprocess(code, ¯oUsage)); + ASSERT_EQUALS(1, macroUsage.size()); + auto it = macroUsage.cbegin(); + ASSERT_EQUALS("DEF_1", it->macroName); + ASSERT_EQUALS(0, it->macroLocation.fileIndex); + ASSERT_EQUALS(1, it->macroLocation.line); + ASSERT_EQUALS(9, it->macroLocation.col); + ASSERT_EQUALS(true, it->macroValueKnown); + ASSERT_EQUALS(0, it->useLocation.fileIndex); + ASSERT_EQUALS(2, it->useLocation.line); + ASSERT_EQUALS(8, it->useLocation.col); + } + { + const char code[] = "#define X 1\n" + "#if defined(X)\n" + "#endif\n"; + std::list macroUsage; + ASSERT_EQUALS("", preprocess(code, ¯oUsage)); + ASSERT_EQUALS(1, macroUsage.size()); + auto it = macroUsage.cbegin(); + ASSERT_EQUALS("X", it->macroName); + ASSERT_EQUALS(2, it->useLocation.line); + ASSERT_EQUALS(13, it->useLocation.col); + } + { + const char code[] = "#define X 1\n" + "#if X\n" + "#endif\n"; + std::list macroUsage; + ASSERT_EQUALS("", preprocess(code, ¯oUsage)); + ASSERT_EQUALS(1, macroUsage.size()); + auto it = macroUsage.cbegin(); + ASSERT_EQUALS("X", it->macroName); + ASSERT_EQUALS(2, it->useLocation.line); + ASSERT_EQUALS(5, it->useLocation.col); + } +} + +static void isAbsolutePath() { +#ifdef _WIN32 + ASSERT_EQUALS(true, simplecpp::isAbsolutePath("C:\\foo\\bar")); + ASSERT_EQUALS(true, simplecpp::isAbsolutePath("C:/foo/bar")); + ASSERT_EQUALS(true, simplecpp::isAbsolutePath("\\\\foo\\bar")); + + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("foo\\bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("foo/bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("foo.cpp")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("C:foo.cpp")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("C:foo\\bar.cpp")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("bar.cpp")); + //ASSERT_EQUALS(true, simplecpp::isAbsolutePath("\\")); // TODO + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("0:\\foo\\bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("0:/foo/bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("\\foo\\bar")); + //ASSERT_EQUALS(false, simplecpp::isAbsolutePath("\\\\")); // TODO + //ASSERT_EQUALS(false, simplecpp::isAbsolutePath("//")); // TODO + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("/foo/bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("/")); +#else + ASSERT_EQUALS(true, simplecpp::isAbsolutePath("/foo/bar")); + ASSERT_EQUALS(true, simplecpp::isAbsolutePath("/")); + ASSERT_EQUALS(true, simplecpp::isAbsolutePath("//host/foo/bar")); + + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("foo/bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("foo.cpp")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("C:\\foo\\bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("C:/foo/bar")); + ASSERT_EQUALS(false, simplecpp::isAbsolutePath("\\\\foo\\bar")); +#endif +} + +// crashes detected by fuzzer static void fuzz_crash() { { @@ -3181,10 +5504,28 @@ static void fuzz_crash() "foo(f##oo(intp))\n"; (void)preprocess(code, simplecpp::DUI()); // do not crash } + { // #546 + const char code[] = "#if __has_include<\n"; + simplecpp::OutputList outputList; + ASSERT_EQUALS("", preprocess(code, &outputList)); // do not crash + ASSERT_EQUALS("file0,1,syntax_error,failed to evaluate #if condition\n", toString(outputList)); + } } -int main(int argc, char **argv) +// memory leaks detected by LSAN/valgrind +static void leak() +{ + { // #498 + const char code[] = "#define e(...)__VA_OPT__()\n" + "#define e\n"; + (void)preprocess(code, simplecpp::DUI()); + } +} + +static void runTests(int argc, char **argv, Input input) { + USE_INPUT = input; + TEST_CASE(backslash); TEST_CASE(builtin); @@ -3199,6 +5540,7 @@ int main(int argc, char **argv) TEST_CASE(comment); TEST_CASE(comment_multiline); + TEST_CASE(keep_comments); TEST_CASE(constFold); @@ -3214,13 +5556,35 @@ int main(int argc, char **argv) TEST_CASE(define6); TEST_CASE(define7); TEST_CASE(define8); + TEST_CASE(macro_c11_example3); + TEST_CASE(macro_c11_example4); + TEST_CASE(macro_c11_example5); + TEST_CASE(macro_c11_example7); + TEST_CASE(macro_c23_va_opt_examples); TEST_CASE(define9); TEST_CASE(define10); TEST_CASE(define11); TEST_CASE(define12); TEST_CASE(define13); + TEST_CASE(define14); // #296 + TEST_CASE(define15); // #231 + TEST_CASE(define16); // #201 + TEST_CASE(define17); // #185 + TEST_CASE(define18); // #130 + TEST_CASE(define19); // #124 + TEST_CASE(define20); // #113 + TEST_CASE(define21); // #66 + TEST_CASE(define22); // #40 + TEST_CASE(define23); // #40 + TEST_CASE(define24); + TEST_CASE(define25); + TEST_CASE(define26); + TEST_CASE(define27); TEST_CASE(define_invalid_1); TEST_CASE(define_invalid_2); + TEST_CASE(define_invalid_3); + TEST_CASE(define_invalid_4); + TEST_CASE(define_wrong_number_of_arguments); TEST_CASE(define_define_1); TEST_CASE(define_define_2); TEST_CASE(define_define_3); @@ -3245,6 +5609,7 @@ int main(int argc, char **argv) TEST_CASE(define_define_21); TEST_CASE(define_define_22); // #400 TEST_CASE(define_define_23); // #403 - crash, infinite recursion + TEST_CASE(define_define_24); // #590 TEST_CASE(define_va_args_1); TEST_CASE(define_va_args_2); TEST_CASE(define_va_args_3); @@ -3256,12 +5621,19 @@ int main(int argc, char **argv) TEST_CASE(define_va_opt_5); TEST_CASE(define_va_opt_6); TEST_CASE(define_va_opt_7); + TEST_CASE(define_va_opt_8); + TEST_CASE(define_va_opt_9); // #632 TEST_CASE(pragma_backslash); // multiline pragma directive + TEST_CASE(pragma_backslash_2); // #217 // UB: #ifdef as macro parameter TEST_CASE(define_ifdef); + TEST_CASE(if_invalid_1); + TEST_CASE(if_invalid_2); + TEST_CASE(if_invalid_3); + TEST_CASE(dollar); TEST_CASE(error1); @@ -3269,11 +5641,19 @@ int main(int argc, char **argv) TEST_CASE(error3); TEST_CASE(error4); TEST_CASE(error5); + TEST_CASE(error6); + TEST_CASE(error7); + TEST_CASE(error8); TEST_CASE(garbage); TEST_CASE(garbage_endif); TEST_CASE(hash); + TEST_CASE(hashEscape); + TEST_CASE(hashComment); + TEST_CASE(hashMacroTrailingWhitespace); + TEST_CASE(hashCombinedOperator); + TEST_CASE(hashEscapeTrailingBackslash); TEST_CASE(hashhash1); TEST_CASE(hashhash2); TEST_CASE(hashhash3); @@ -3304,11 +5684,19 @@ int main(int argc, char **argv) TEST_CASE(hashhash_invalid_missing_args); TEST_CASE(hashhash_null_stmt); TEST_CASE(hashhash_empty_va_args); + TEST_CASE(hashhash_punctuator); + TEST_CASE(hashhash_punctuator_invalid); + TEST_CASE(hashhash_empty_placemarker); + TEST_CASE(hashhash_invalid_dotdot); + TEST_CASE(hashhash_invalid_name_string); + TEST_CASE(hashhash_va_args_unexpected); + // C standard, 5.1.1.2, paragraph 4: // If a character sequence that matches the syntax of a universal // character name is produced by token concatenation (6.10.3.3), // the behavior is undefined." TEST_CASE(hashhash_universal_character); + TEST_CASE(hashhash_universal_character_2); // c++17 __has_include TEST_CASE(has_include_1); @@ -3318,6 +5706,11 @@ int main(int argc, char **argv) TEST_CASE(has_include_5); TEST_CASE(has_include_6); + TEST_CASE(define_has_include_invalid_1); + TEST_CASE(define_has_include_invalid_2); + TEST_CASE(define_has_include_invalid_3); + TEST_CASE(define_has_include_invalid_4); + TEST_CASE(strict_ansi_1); TEST_CASE(strict_ansi_2); TEST_CASE(strict_ansi_3); @@ -3327,6 +5720,7 @@ int main(int argc, char **argv) TEST_CASE(ifdef2); TEST_CASE(ifndef); TEST_CASE(ifA); + TEST_CASE(ifFloatingPointLiteral); // #612 TEST_CASE(ifCharLiteral); TEST_CASE(ifDefined); TEST_CASE(ifDefinedNoPar); @@ -3336,13 +5730,23 @@ int main(int argc, char **argv) TEST_CASE(ifDefinedInvalid2); TEST_CASE(ifDefinedHashHash); TEST_CASE(ifDefinedHashHash2); + TEST_CASE(definedAtEndOfFile); TEST_CASE(ifLogical); + TEST_CASE(ifLogicalPrecedence); TEST_CASE(ifSizeof); TEST_CASE(elif); + TEST_CASE(elifdef); + TEST_CASE(elifndef); TEST_CASE(ifif); TEST_CASE(ifoverflow); TEST_CASE(ifdiv0); + TEST_CASE(ifShortCircuit); + TEST_CASE(ifZeroCondition); + TEST_CASE(ifUnaryOperators); + TEST_CASE(ifInvalidExpression); + TEST_CASE(ifTrueFalse); TEST_CASE(ifalt); // using "and", "or", etc + TEST_CASE(ifAltUnaryOperators); TEST_CASE(ifexpr); TEST_CASE(ifUndefFuncStyleMacro); @@ -3351,6 +5755,13 @@ int main(int argc, char **argv) TEST_CASE(location3); TEST_CASE(location4); TEST_CASE(location5); + TEST_CASE(location6); + TEST_CASE(location7); + TEST_CASE(location8); + TEST_CASE(location9); + TEST_CASE(location10); + TEST_CASE(location11); + TEST_CASE(location12); TEST_CASE(missingHeader1); TEST_CASE(missingHeader2); @@ -3364,6 +5775,8 @@ int main(int argc, char **argv) TEST_CASE(nullDirective2); TEST_CASE(nullDirective3); + TEST_CASE(lineDirective); + TEST_CASE(include1); TEST_CASE(include2); TEST_CASE(include3); @@ -3373,6 +5786,8 @@ int main(int argc, char **argv) TEST_CASE(include7); // #include MACRO TEST_CASE(include8); // #include MACRO(X) TEST_CASE(include9); // #include MACRO + TEST_CASE(include10); // -include with load() + TEST_CASE(include11); // -include with preprocess() TEST_CASE(multiline1); TEST_CASE(multiline2); @@ -3394,6 +5809,9 @@ int main(int argc, char **argv) TEST_CASE(readfile_unhandled_chars); TEST_CASE(readfile_error); TEST_CASE(readfile_file_not_found); + TEST_CASE(readfile_no_eof_newline); + TEST_CASE(readfile_cr_newlines); + TEST_CASE(readfile_filestream); TEST_CASE(stringify1); @@ -3402,6 +5820,16 @@ int main(int argc, char **argv) TEST_CASE(tokenMacro3); TEST_CASE(tokenMacro4); TEST_CASE(tokenMacro5); + TEST_CASE(tokenMacro6); + TEST_CASE(tokenMacro7); + TEST_CASE(tokenMacro8); + TEST_CASE(tokenMacro9); + TEST_CASE(tokenMacro10); + TEST_CASE(tokenMacro11); + TEST_CASE(tokenMacro12); + TEST_CASE(tokenMacro13); + TEST_CASE(tokenMacro14); + TEST_CASE(tokenMacro15); TEST_CASE(undef); @@ -3435,7 +5863,23 @@ int main(int argc, char **argv) TEST_CASE(preprocess_files); + TEST_CASE(tokenlist_api); + + TEST_CASE(isAbsolutePath); + + TEST_CASE(bad_macro_syntax); + + TEST_CASE(ifCond); + TEST_CASE(macroUsage); + TEST_CASE(fuzz_crash); + TEST_CASE(leak); +} + +int main(int argc, char **argv) +{ + runTests(argc, argv, Input::Stringstream); + runTests(argc, argv, Input::CharBuffer); return numberOfFailedAssertions > 0 ? EXIT_FAILURE : EXIT_SUCCESS; }