diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 786e3d5c..e91fadf4 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @confluentinc/clients
+* @confluentinc/clients @confluentinc/data-governance
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index cb301559..4724334d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -9,11 +9,11 @@ about: Create a report to help us improve
- Node Version [e.g. 8.2.1]:
- NPM Version [e.g. 5.4.2]:
- C++ Toolchain [e.g. Visual Studio, llvm, g++]:
- - confluent-kafka-js version [e.g. 2.3.3]:
+ - confluent-kafka-javascript version [e.g. 2.3.3]:
**Steps to Reproduce**
-**confluent-kafka-js Configuration Settings**
+**confluent-kafka-javascript Configuration Settings**
**Additional context**
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index c547ffba..0a21a501 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -1,7 +1,7 @@
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
-name: Publish confluent-kafka-js
+name: Publish confluent-kafka-javascript
on:
release:
diff --git a/.gitignore b/.gitignore
index 1b6d3a6a..603212d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,19 @@
build/
+dist/
node_modules/
deps/librdkafka
npm-debug.log
docs
+examples/**/package-lock.json
+
deps/*
!deps/*.gyp
!deps/windows-install.*
.DS_Store
+.idea
.vscode
+coverage
diff --git a/.gitmodules b/.gitmodules
index 4d4e7fb9..63dc1ac1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
[submodule "deps/librdkafka"]
path = deps/librdkafka
- url = https://github.com/edenhill/librdkafka.git
+ url = https://github.com/confluentinc/librdkafka.git
diff --git a/.jshintignore b/.jshintignore
deleted file mode 100644
index b43bf86b..00000000
--- a/.jshintignore
+++ /dev/null
@@ -1 +0,0 @@
-README.md
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index 09968b8b..00000000
--- a/.jshintrc
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "node": true,
- "mocha": true,
- "browser": false,
- "boss": true,
- "curly": true,
- "debug": false,
- "devel": false,
- "eqeqeq": true,
- "evil": true,
- "forin": false,
- "latedef": false,
- "noarg": true,
- "nonew": true,
- "nomen": false,
- "onevar": false,
- "plusplus": false,
- "regexp": false,
- "undef": true,
- "strict": false,
- "white": false,
- "eqnull": true
-}
diff --git a/.npmignore b/.npmignore
index c6793a26..418ac7aa 100644
--- a/.npmignore
+++ b/.npmignore
@@ -5,6 +5,9 @@ deps/*
.gitmodules
Dockerfile
deps/librdkafka/config.h
+schemaregistry
+schemaregistry-examples
build
.github
.vscode
+.semaphore
\ No newline at end of file
diff --git a/.semaphore/build-docker.sh b/.semaphore/build-docker.sh
new file mode 100755
index 00000000..3f148428
--- /dev/null
+++ b/.semaphore/build-docker.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+# This script is used to build the project within a docker image.
+# The docker image is assumed to be an alpine docker image, for glibc based builds, we use
+# the semaphhore agent directly.
+
+apk add -U ca-certificates openssl ncurses coreutils python3 make gcc g++ libgcc linux-headers grep util-linux binutils findutils perl patch musl-dev bash
+# /v is the volume mount point for the project root
+cd /v
+npm install
+npx node-pre-gyp package
diff --git a/.semaphore/project.yml b/.semaphore/project.yml
index e20c3839..7bef71a4 100644
--- a/.semaphore/project.yml
+++ b/.semaphore/project.yml
@@ -6,12 +6,12 @@
apiVersion: v1alpha
kind: Project
metadata:
- name: confluent-kafka-js
+ name: confluent-kafka-javascript
description: ""
spec:
visibility: private
repository:
- url: git@github.com:confluentinc/confluent-kafka-js.git
+ url: git@github.com:confluentinc/confluent-kafka-javascript.git
run_on:
- branches
- tags
diff --git a/.semaphore/project_public.yml b/.semaphore/project_public.yml
new file mode 100644
index 00000000..fb83ce58
--- /dev/null
+++ b/.semaphore/project_public.yml
@@ -0,0 +1,25 @@
+# This file is managed by ServiceBot plugin - Semaphore. The content in this file is created using a common
+# template and configurations in service.yml.
+# Modifications in this file will be overwritten by generated content in the nightly run.
+# For more information, please refer to the page:
+# https://confluentinc.atlassian.net/wiki/spaces/Foundations/pages/2871296194/Add+SemaphoreCI
+apiVersion: v1alpha
+kind: Project
+metadata:
+ name: confluent-kafka-javascript
+ description: ""
+spec:
+ visibility: public
+ repository:
+ url: git@github.com:confluentinc/confluent-kafka-javascript.git
+ run_on:
+ - forked_pull_requests
+ pipeline_file: .semaphore/semaphore.yml
+ integration_type: github_app
+ status:
+ pipeline_files:
+ - path: .semaphore/semaphore.yml
+ level: pipeline
+ forked_pull_requests:
+ allowed_contributors:
+ - "ConfluentSemaphore"
diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml
index 24f504ca..a3d82cf2 100644
--- a/.semaphore/semaphore.yml
+++ b/.semaphore/semaphore.yml
@@ -15,7 +15,7 @@ auto_cancel:
when: "branch != 'master'"
execution_time_limit:
- hours: 1
+ hours: 3
queue:
- when: "branch != 'master'"
@@ -25,37 +25,492 @@ global_job_config:
prologue:
commands:
- checkout
- - make show-args
- - . vault-setup
- - . vault-sem-get-secret ci-reporting
- - . vault-sem-get-secret v1/ci/kv/service-foundations/cc-mk-include
- - make init-ci
- epilogue:
- always:
- commands:
- - make epilogue-ci
+ - git submodule update --init --recursive
+ - cd deps/librdkafka
+ - git fetch origin
+ - git checkout v2.5.3
+ - cd ../../
+ - cache clear
blocks:
- - name: "Build, Test, Release"
- run:
- # don't run the build or unit tests on non-functional changes...
- when: "change_in('/', {exclude: ['/.deployed-versions/', '.github/']})"
+ - name: "Linux amd64 (musl): Build and test"
+ dependencies: [ ]
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-amd64-2
+ prologue:
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ jobs:
+ - name: "Build from source and test for musl"
+ commands:
+ - docker run -v "$(pwd):/v" node:18-alpine /v/.semaphore/build-docker.sh
+
+ - name: "Linux arm64 (musl): Build and test"
+ dependencies: [ ]
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-arm64-1
+ prologue:
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ jobs:
+ - name: "Build from source and test for musl"
+ commands:
+ - docker run -v "$(pwd):/v" node:18-alpine /v/.semaphore/build-docker.sh
+
+ - name: "Linux arm64: Build and test"
+ dependencies: [ ]
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-arm64-1
+ jobs:
+ - name: "Build from source and test"
+ commands:
+ - npm install # this will actually not build anything if we have a release, but rather, fetch things using node-pre-gyp - so change this later.
+ - make test
+
+ - name: 'macOS arm64/m1: Build and test'
+ dependencies: []
+ task:
+ agent:
+ machine:
+ type: s1-prod-macos-13-5-arm64
+ jobs:
+ - name: 'Build from source and test'
+ commands:
+ - npm install # this will actually not build anything if we have a release, but rather, fetch things using node-pre-gyp - so change this later.
+ - make test
+
+ - name: "Linux amd64: Build, test, lint"
+ dependencies: [ ]
task:
- # You can customize your CI job here
-# env_vars:
-# # custom env_vars
-# prologue:
-# commands:
-# # custom vault secrets
-# # custom prologue commands
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-amd64-2
+ prologue:
+ commands:
+ - npm install # this will actually not build anything if we have a release, but rather, fetch things using node-pre-gyp - so change this later.
jobs:
- - name: "Build, Test, Release"
+ - name: "Test"
commands:
- - make build
- make test
- - make release-ci
- epilogue:
- always:
+ - name: "Promisified Tests"
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ - docker compose up -d && sleep 30
+ - export NODE_OPTIONS='--max-old-space-size=1536'
+ - npx jest --forceExit --no-colors --ci test/promisified/admin/delete_groups.spec.js test/promisified/consumer/pause.spec.js
+ - name: "ESLint"
+ commands:
+ - npx eslint lib/kafkajs
+
+ - name: "Linux amd64: Performance"
+ dependencies: [ ]
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-amd64-4
+ env_vars:
+ - name: TARGET_PRODUCE_PERFORMANCE
+ value: "35"
+ - name: TARGET_CONSUME_PERFORMANCE
+ value: "18"
+ - name: TARGET_CTP_PERFORMANCE
+ value: "0.02"
+ prologue:
+ commands:
+ - wget -qO - https://packages.confluent.io/deb/7.7/archive.key | sudo apt-key add -
+ - sudo add-apt-repository "deb https://packages.confluent.io/clients/deb $(lsb_release -cs) main"
+ - sudo apt-get update
+ - sudo apt-get install -y build-essential gcc g++ make python3
+ - sudo apt-get install -y librdkafka-dev bc
+ - export CKJS_LINKING=dynamic
+ - export BUILD_LIBRDKAFKA=0
+ - npm install
+ - npx node-pre-gyp --build-from-source clean
+ - npx node-pre-gyp --build-from-source configure
+ - npx node-pre-gyp --build-from-source build
+ jobs:
+ - name: "Performance Test"
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ - docker compose up -d && sleep 30
+ - export NODE_OPTIONS='--max-old-space-size=1536'
+ - cd examples/performance
+ - npm install
+ - bash -c '../../ci/tests/run_perf_test.sh'
+
+ - name: "Linux amd64: Release"
+ dependencies: [ ]
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-amd64-2
+ env_vars:
+ - name: ARCHITECTURE
+ value: "x64"
+ - name: PLATFORM
+ value: "linux"
+ - name: LIBC
+ value: "glibc"
+ prologue:
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ jobs:
+ - name: "Release: LTS:18"
+ commands:
+ - sem-version node 18.19.0
+ - export NODE_ABI=108
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: LTS:20"
+ commands:
+ - sem-version node 20.10.0
+ - export NODE_ABI=115
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 21"
+ commands:
+ - sem-version node 21.4.0
+ - export NODE_ABI=120
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 22"
+ commands:
+ - sem-version node 22.2.0
+ - export NODE_ABI=127
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+
+ - name: "Linux arm64: Release"
+ dependencies: [ ]
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-arm64-1
+ env_vars:
+ - name: ARCHITECTURE
+ value: "arm64"
+ - name: PLATFORM
+ value: "linux"
+ - name: LIBC
+ value: "glibc"
+ prologue:
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ jobs:
+ - name: "Release: LTS:18"
+ commands:
+ - sem-version node 18.19.0
+ - export NODE_ABI=108
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: LTS:20"
+ commands:
+ - sem-version node 20.10.0
+ - export NODE_ABI=115
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 21"
+ commands:
+ - sem-version node 21.4.0
+ - export NODE_ABI=120
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 22"
+ commands:
+ - sem-version node 22.2.0
+ - export NODE_ABI=127
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+
+ - name: "Linux amd64 musl: Release"
+ dependencies: [ ]
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-amd64-1
+ env_vars:
+ - name: ARCHITECTURE
+ value: "x64"
+ - name: PLATFORM
+ value: "linux"
+ - name: LIBC
+ value: "musl"
+ prologue:
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ jobs:
+ - name: "Release: LTS:18"
+ commands:
+ - export NODE_ABI=108
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:18-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: LTS:20"
+ commands:
+ - export NODE_ABI=115
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:20-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 21"
+ commands:
+ - export NODE_ABI=120
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:21-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 22"
+ commands:
+ - export NODE_ABI=127
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:22-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+
+ - name: "Linux arm64 musl: Release"
+ dependencies: [ ]
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-arm64-1
+ env_vars:
+ - name: ARCHITECTURE
+ value: "arm64"
+ - name: PLATFORM
+ value: "linux"
+ - name: LIBC
+ value: "musl"
+ prologue:
+ commands:
+ - '[[ -z $DOCKERHUB_APIKEY ]] || docker login --username $DOCKERHUB_USER --password $DOCKERHUB_APIKEY'
+ jobs:
+ - name: "Release: LTS:18"
+ commands:
+ - export NODE_ABI=108
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:18-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: LTS:20"
+ commands:
+ - export NODE_ABI=115
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:20-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 21"
+ commands:
+ - export NODE_ABI=120
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:21-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 22"
+ commands:
+ - export NODE_ABI=127
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-linux-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - docker run -v "$(pwd):/v" node:22-alpine /v/.semaphore/build-docker.sh
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+
+ - name: "macOS arm64/m1: Release"
+ dependencies: [ ]
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-macos-13-5-arm64
+ env_vars:
+ - name: ARCHITECTURE
+ value: "arm64"
+ - name: PLATFORM
+ value: "darwin"
+ - name: LIBC
+ value: "unknown"
+ jobs:
+ - name: "Release: LTS:18"
+ commands:
+ - sem-version node 18.19.0
+ - export NODE_ABI=108
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: LTS:20"
+ commands:
+ - sem-version node 20.10.0
+ - export NODE_ABI=115
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 21"
+ commands:
+ - sem-version node 21.4.0
+ - export NODE_ABI=120
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+ - name: "Release: latest: 22"
+ commands:
+ - sem-version node 22.2.0
+ - export NODE_ABI=127
+ - export ARTIFACT_KEY="confluent-kafka-javascript-${SEMAPHORE_GIT_TAG_NAME}-node-v${NODE_ABI}-${PLATFORM}-${LIBC}-${ARCHITECTURE}.tar.gz"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}
+ - artifact push workflow "build/stage/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/${ARTIFACT_KEY}"
+
+ - name: "Windows x64: Release"
+ dependencies: [ ]
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-windows
+ env_vars:
+ # Disable vcpkg telemetry
+ - name: VCPKG_DISABLE_METRICS
+ value: 'yes'
+ - name: ARCHITECTURE
+ value: "x64"
+ - name: PLATFORM
+ value: "win32"
+ - name: LIBC
+ value: "unknown"
+ prologue:
+ commands:
+ # The semaphore agent already comes with an installed version of node. We, however, need to use a different
+ # version of node for the release (as many as we need to cover all the different ABIs).
+ # The node installer does not allow us to downgrade, so we need to uninstall the current version.
+ # The method below isn't particularly robust (as it depends on the particular format of the URL), but it
+ # works and can be easily fixed if it breaks (the node --version in the below jobs can be checked if there are
+ # any issues in the build).
+ - $env:InstalledMajor = (Get-Command node).Version.Major
+ - $env:InstalledMinor = (Get-Command node).Version.Minor
+ - $env:InstalledBuild = (Get-Command node).Version.Build
+ - $env:InstalledVersion = "v${env:InstalledMajor}.${env:InstalledMinor}.${env:InstalledBuild}"
+ - echo "https://nodejs.org/dist/${env:InstalledVersion}/node-${env:InstalledVersion}-x64.msi"
+ - Invoke-WebRequest "https://nodejs.org/dist/${env:InstalledVersion}/node-${env:InstalledVersion}-x64.msi" -OutFile node_old.msi
+ - msiexec /qn /l* node-old-log.txt /uninstall node_old.msi
+ - cat .\node-old-log.txt
+ jobs:
+ - name: "Release: LTS:18"
+ commands:
+ - Invoke-WebRequest "https://nodejs.org/download/release/v18.19.0/node-v18.19.0-x64.msi" -OutFile node.msi
+ - msiexec /qn /l* node-log.txt /i node.msi
+ - cat .\node-log.txt
+ - node --version
+ - pip install setuptools
+ - $env:NODE_ABI = 108
+ - $env:ARTIFACT_KEY = "confluent-kafka-javascript-${env:SEMAPHORE_GIT_TAG_NAME}-node-v${env:NODE_ABI}-${env:PLATFORM}-${env:LIBC}-${env:ARCHITECTURE}.tar.gz"
+ - echo "$env:ARTIFACT_KEY"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - artifact push workflow "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}" --destination "releases/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - name: "Release: LTS:20"
+ commands:
+ - Invoke-WebRequest "https://nodejs.org/dist/v20.11.0/node-v20.11.0-x64.msi" -OutFile node.msi
+ - msiexec /qn /l* node-log.txt /i node.msi
+ - node --version
+ - pip install setuptools
+ - $env:NODE_ABI = 115
+ - $env:ARTIFACT_KEY = "confluent-kafka-javascript-${env:SEMAPHORE_GIT_TAG_NAME}-node-v${env:NODE_ABI}-${env:PLATFORM}-${env:LIBC}-${env:ARCHITECTURE}.tar.gz"
+ - echo "$env:ARTIFACT_KEY"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - artifact push workflow "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}" --destination "releases/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - name: "Release: latest: 21"
+ commands:
+ - Invoke-WebRequest "https://nodejs.org/dist/v21.6.1/node-v21.6.1-x64.msi" -OutFile node.msi
+ - msiexec /qn /l* node-log.txt /i node.msi
+ - node --version
+ - pip install setuptools
+ - $env:NODE_ABI = 120
+ - $env:ARTIFACT_KEY = "confluent-kafka-javascript-${env:SEMAPHORE_GIT_TAG_NAME}-node-v${env:NODE_ABI}-${env:PLATFORM}-${env:LIBC}-${env:ARCHITECTURE}.tar.gz"
+ - echo "$env:ARTIFACT_KEY"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - artifact push workflow "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}" --destination "releases/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - name: "Release: latest: 22"
+ commands:
+ - Invoke-WebRequest "https://nodejs.org/dist/v22.2.0/node-v22.2.0-x64.msi" -OutFile node.msi
+ - msiexec /qn /l* node-log.txt /i node.msi
+ - node --version
+ - pip install setuptools
+ - $env:NODE_ABI = 127
+ - $env:ARTIFACT_KEY = "confluent-kafka-javascript-${env:SEMAPHORE_GIT_TAG_NAME}-node-v${env:NODE_ABI}-${env:PLATFORM}-${env:LIBC}-${env:ARCHITECTURE}.tar.gz"
+ - echo "$env:ARTIFACT_KEY"
+ - npm install # node-pre-gyp will fallback to build here, because new tag implies no release yet.
+ - npx node-pre-gyp package
+ - ls "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+ - artifact push workflow "build/stage/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}" --destination "releases/${env:SEMAPHORE_GIT_TAG_NAME}/${env:ARTIFACT_KEY}"
+
+ - name: 'Packaging: tar all release artifacts'
+ dependencies:
+ - 'Linux amd64: Release'
+ - 'Linux arm64: Release'
+ - 'Linux amd64 musl: Release'
+ - 'Linux arm64 musl: Release'
+ - 'macOS arm64/m1: Release'
+ - 'Windows x64: Release'
+ run:
+ when: "tag =~ '^v[0-9]\\.'"
+ task:
+ agent:
+ machine:
+ type: s1-prod-ubuntu20-04-amd64-2
+ jobs:
+ - name: "Tarball"
commands:
- - make epilogue-ci
- - make testbreak-after
+ - artifact pull workflow releases
+ - tar -czvf releases.${SEMAPHORE_GIT_TAG_NAME}.tar.gz releases
+ - artifact push project "releases.${SEMAPHORE_GIT_TAG_NAME}.tar.gz" --destination "releases/${SEMAPHORE_GIT_TAG_NAME}/releases.${SEMAPHORE_GIT_TAG_NAME}.tar.gz"
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..11802730
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,142 @@
+# confluent-kafka-javascript v0.2.1
+
+v0.2.1 is a limited availability release. It is supported for all usage.
+
+## Features
+
+1. Update README, docs, and examples for Confluent's Schema Registry client.
+
+
+# confluent-kafka-javascript v0.2.0
+
+v0.2.0 is a limited availability release. It is supported for all usage.
+
+## Features
+
+1. Switch to using `librdkafka` on the latest released tag `v2.5.3` instead of `master`.
+
+
+# confluent-kafka-javascript v0.1.17-devel
+
+v0.1.17-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add a commitCb method to the callback-based API which allows committing asynchronously.
+2. Pass assign/unassign functions to the rebalance callback in the promisified API, allowing
+ the user to control the assignment of partitions, or pause just after a rebalance.
+3. Remove store from promisified API and let the library handle all the stores.
+4. Add JavaScript-level debug logging to the client for debugging issues within the binding.
+5. Various fixes for performance and robustness of the consumer cache.
+6. Remove `consumerGroupId` argument from the `sendOffsets` method of the transactional producer,
+ and instead, only allow using a `consumer`.
+
+## Fixes
+
+1. Do not modify RegExps which don't start with a ^, instead, throw an error so
+ that there is no unexpected behaviour for the user (Issue [#64](https://github.com/confluentinc/confluent-kafka-javascript/issues/64)).
+2. Do not mutate arguments in run, pause and resume (Issue [#61](https://github.com/confluentinc/confluent-kafka-javascript/issues/61)).
+3. Fix a segmentation fault in `listGroups` when passing `matchConsumerGroupStates` as undefined.
+
+
+# confluent-kafka-javascript v0.1.16-devel
+
+v0.1.16-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add per-partition concurrency to consumer.
+2. Add true `eachBatch` support to consumer.
+3. Add a `leaderEpoch` field to the topic partitions where required (listing, committing, etc.).
+
+
+# confluent-kafka-javascript v0.1.15-devel
+
+v0.1.15-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add Node v22 builds and bump librdkafka version on each version bump of this library.
+
+
+# confluent-kafka-javascript v0.1.14-devel
+
+v0.1.14-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add metadata to offset commit and offset store (non-promisified API).
+2. Add types for logger and loglevel to configuration.
+3. Add Producer polling from background thread. This improves performance for cases when send is awaited on.
+4. Enable consume optimization from v0.1.13-devel (Features #2) by default for the promisified API.
+
+## Bug Fixes
+
+1. Fix issues with the header conversions from promisified API to the non-promisified API to match
+ the type signature and allow Buffers to be passed as header values in the C++ layer.
+
+
+# confluent-kafka-javascript v0.1.13-devel
+
+v0.1.13-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add support for `storeOffsets` in the consumer API.
+2. Add optimization while consuming, in cases where the size of messages pending in our subscription is less than the consumer cache size.
+
+## Bug Fixes
+
+1. Fix memory leak in incremental assign (@martijnimhoff, #35).
+2. Fix various issues with typings, and reconcile typings, JavaScript code, and MIGRATION.md to be consistent.
+
+
+# confluent-kafka-javascript v0.1.12-devel
+
+v0.1.12-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add support for `listTopics` in the Admin API.
+2. Add support for OAUTHBEARER token refresh callback for both promisified and non promisified API.
+
+## Bug Fixes
+
+1. Fix aliasing bug between `NodeKafka::Conf` and `RdKafka::ConfImpl`.
+2. Fix issue where `assign/unassign` were called instead of `incrementalAssign/incrementalUnassign` while using
+ the Cooperative Sticky assigner, and setting the `rebalance_cb` as a boolean rather than as a function.
+3. Fix memory leaks in Dispatcher and Conf (both leaked memory at client close).
+4. Fix type definitions and make `KafkaJS` and `RdKafka` separate namespaces, while maintaining compatibility
+ with node-rdkafka's type definitions.
+
+
+# confluent-kafka-javascript v0.1.11-devel
+
+v0.1.11-devel is a pre-production, early-access release.
+
+## Features
+
+1. Add support for `eachBatch` in the Consumer API (partial support for API compatibility).
+2. Add support for `listGroups`, `describeGroups` and `deleteGroups` in the Admin API.
+
+
+# confluent-kafka-javascript v0.1.10-devel
+
+v0.1.10-devel is a pre-production, early-access release.
+
+## Features
+
+1. Pre-built binaries for Windows (x64) added on an experimental basis.
+
+
+# confluent-kafka-javascript v0.1.9-devel
+
+v0.1.9-devel is a pre-production, early-access release.
+
+## Features
+
+1. Pre-built binaries for Linux (both amd64 and arm64, both musl and glibc), for macOS (m1), for node versions 18, 20 and 21.
+2. Promisified API for Consumer, Producer and Admin Client.
+3. Allow passing topic configuration properties via the global configuration block.
+4. Remove dependencies with security issues.
+5. Support for the Cooperative Sticky assignor.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e2cabe1f..e718a27f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,39 +1,17 @@
-# Contributing to `confluent-kafka-js`
+# Contributing to `confluent-kafka-javascript`
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
-The following is a set of guidelines for contributing to `confluent-kafka-js`
+The following is a set of guidelines for contributing to `confluent-kafka-javascript`
which is hosted by [Confluent Inc.](https://github.com/confluentinc)
on GitHub. This document lists rules, guidelines, and help getting started,
so if you feel something is missing feel free to send a pull request.
-#### Table Of Contents
-
-[What should I know before I get started?](#what-should-i-know-before-i-get-started)
- * [Contributor Agreement](#contributor-agreement)
-
-[How Can I Contribute?](#how-can-i-contribute)
- * [Reporting Bugs](#reporting-bugs)
- * [Suggesting Enhancements](#suggesting-enhancements)
- * [Pull Requests](#pull-requests)
-
-[Styleguides](#styleguides)
- * [Git Commit Messages](#git-commit-messages)
- * [JavaScript Styleguide](#javascript-styleguide)
- * [C++ Styleguide](#c++-styleguide)
- * [Specs Styleguide](#specs-styleguide)
- * [Documentation Styleguide](#documentation-styleguide)
-
-[Debugging](#debugging)
- * [Debugging C++](#debugging-c)
-
-[Updating librdkafka version](#updating-librdkafka-version)
-
## What should I know before I get started?
### Contributor Agreement
-Not currently required.
+Required (please follow instructions after making any Pull Requests).
## How can I contribute?
@@ -49,6 +27,10 @@ replicas, partitions, and brokers you are connecting to, because some issues
might be related to Kafka. A list of `librdkafka` configuration key-value pairs
also helps.
+Adding the property `debug` in your `librdkafka` configuration will help us. A list of
+possible values is available [here](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md),
+but you can set it to `all` if verbose logs are okay.
+
### Suggesting Enhancements
Please use __Github Issues__ to suggest enhancements. We are happy to consider
@@ -61,7 +43,7 @@ library's core.
* Include new test cases (either end-to-end or unit tests) with your change.
* Follow our style guides.
-* Make sure all tests are still passing and the `linter` does not report any issues.
+* Make sure all tests are still passing and the linter does not report any issues.
* End files with a new line.
* Document the new code in the comments (if it is JavaScript) so the
documentation generator can update the reference documentation.
@@ -103,15 +85,8 @@ In short:
### JavaScript Styleguide
-* Place `module.exports` at or near the top of the file.
- * Defined functions are hoisted, so it is appropriate to define the
- function after you export it.
- * When exporting an object, define it first, then export it, and then add
- methods or properties.
-* Do not use ES2015 specific features (for example, do not use `let`, `const`,
- or `class`).
* All callbacks should follow the standard Node.js callback signature.
-* Your JavaScript should properly pass the linter (`make jslint`).
+* Your JavaScript should properly pass the linter (`make jslint` and `make eslint`).
### C++ Styleguide
@@ -121,7 +96,8 @@ In short:
### Specs Styleguide
-* Write all JavaScript tests by using the `mocha` testing framework.
+* Write JavaScript tests by using the `mocha` testing framework for the
+ non-promisified API and `jest` for the promisified API.
* All `mocha` tests should use exports syntax.
* All `mocha` test files should be suffixed with `.spec.js` instead of `.js`.
* Unit tests should mirror the JavaScript files they test (for example,
@@ -144,7 +120,7 @@ In short:
## Editor
-Using Visual Studio code to develop on `confluent-kafka-js`. If you use it you can configure the C++ plugin to resolve the paths needed to inform your intellisense. This is the config file I am using on a mac to resolve the required paths:
+Using Visual Studio code to develop on `confluent-kafka-javascript`. If you use it you can configure the C++ plugin to resolve the paths needed to inform your intellisense. This is the config file I am using on a mac to resolve the required paths:
`c_cpp_properties.json`
```
@@ -176,6 +152,26 @@ Using Visual Studio code to develop on `confluent-kafka-js`. If you use it you c
}
```
+## Tests
+
+This project includes three types of tests in this project:
+* end-to-end integration tests (`mocha`)
+* unit tests (`mocha`)
+* integration tests for promisified API (`jest`)
+
+You can run all types of tests by using `Makefile`. Doing so calls `mocha` or `jest` in your locally installed `node_modules` directory.
+
+* Before you run the tests, be sure to init and update the submodules:
+ 1. `git submodule init`
+ 2. `git submodule update`
+* To run the unit tests, you can run `make lint` or `make test`.
+* To run the promisified integration tests, you can use `make promisified_test`.
+ You must have a running Kafka installation available. By default, the test tries to connect to `localhost:9092`;
+ however, you can supply the `KAFKA_HOST` environment variable to override this default behavior.
+* To run the integration tests, you can use `make e2e`.
+ You must have a running Kafka installation available. By default, the test tries to connect to `localhost:9092`;
+ however, you can supply the `KAFKA_HOST` environment variable to override this default behavior. Run `make e2e`.
+
## Debugging
### Debugging C++
@@ -193,12 +189,22 @@ gdb node
You can add breakpoints and so on after that.
+### Debugging and Profiling JavaScript
+
+Run the code with the `--inspect` flag, and then open `chrome://inspect` in Chrome and connect to the debugger.
+
+Example:
+
+```
+node --inspect path/to/file.js
+```
+
## Updating librdkafka version
-The librdkafka should be periodically updated to the latest release in https://github.com/edenhill/librdkafka/releases
+The librdkafka should be periodically updated to the latest release in https://github.com/confluentinc/librdkafka/releases
Steps to update:
-1. Update the `librdkafka` property in [`package.json`](https://github.com/confluentinc/confluent-kafka-js/blob/master/package.json) to the desired version.
+1. Update the `librdkafka` property in [`package.json`](https://github.com/confluentinc/confluent-kafka-javascript/blob/master/package.json) to the desired version.
1. Update the librdkafka git submodule to that versions release commit (example below)
@@ -209,20 +215,27 @@ Steps to update:
If you get an error during that checkout command, double check that the submodule was initialized / cloned! You may need to run `git submodule update --init --recursive`
-1. Update [`config.d.ts`](https://github.com/confluentinc/confluent-kafka-js/blob/master/config.d.ts) and [`errors.d.ts`](https://github.com/confluentinc/confluent-kafka-js/blob/master/errors.d.ts) TypeScript definitions by running:
+1. Update [`config.d.ts`](https://github.com/confluentinc/confluent-kafka-javascript/blob/master/config.d.ts) and [`errors.d.ts`](https://github.com/confluentinc/confluent-kafka-javascript/blob/master/errors.d.ts) TypeScript definitions by running:
```bash
node ci/librdkafka-defs-generator.js
```
- Note: This is ran automatically during CI flows but it's good to run it during the version upgrade pull request.
1. Run `npm install --lockfile-version 2` to build with the new version and fix any build errors that occur.
1. Run unit tests: `npm run test`
-1. Update the version numbers referenced in the [`README.md`](https://github.com/confluentinc/confluent-kafka-js/blob/master/README.md) file to the new version.
+1. Update the version numbers referenced in the [`README.md`](https://github.com/confluentinc/confluent-kafka-javascript/blob/master/README.md) file to the new version.
+
+## Releasing
+
+1. Increment the `version` in `package.json`. Change the version in `client.js` and `README.md`. Change the librdkafka version in `semaphore.yml` and in `package.json`.
+
+1. Run `npm install` to update the `package-lock.json` file.
-## Publishing new npm version
+1. Create a PR and merge the above changes, and tag the merged commit with the new version, e.g. `git tag vx.y.z && git push origin vx.y.z`.
+ This should be the same string as `version` in `package.json`.
-1. Increment the `version` in `package.json` and merge that change in.
+1. The CI will run on the tag, which will create the release artifacts in Semaphore CI.
-1. Create a new github release. Set the tag & release title to the same string as `version` in `package.json`.
+1. Create a new GitHub release with the tag, and upload the release artifacts from Semaphore CI.
+ The release title should be the same string as `version` in `package.json`.
\ No newline at end of file
diff --git a/INTRODUCTION.md b/INTRODUCTION.md
new file mode 100644
index 00000000..ecbb3c2a
--- /dev/null
+++ b/INTRODUCTION.md
@@ -0,0 +1,546 @@
+# Introduction to Confluent-Kafka-JavaScript
+
+## Configuration
+
+You can pass many configuration options to `librdkafka`. A full list can be found in `librdkafka`'s [Configuration.md](https://github.com/confluentinc/librdkafka/blob/v2.3.0/CONFIGURATION.md)
+
+Configuration keys that have the suffix `_cb` are designated as callbacks. Some
+of these keys are informational and you can choose to opt-in (for example, `dr_cb`). Others are callbacks designed to
+return a value, such as `partitioner_cb`.
+
+Not all of these options are supported.
+The library will throw an error if the value you send in is invalid.
+
+The library currently supports the following callbacks:
+* `partitioner_cb`
+* `dr_cb` or `dr_msg_cb`
+* `event_cb`
+* `rebalance_cb` (see [Rebalancing](#rebalancing))
+* `offset_commit_cb` (see [Commits](#commits))
+
+### Librdkafka Methods
+
+This library includes two utility functions for detecting the status of your installation. Please try to include these when making issue reports where applicable.
+
+You can get the features supported by your compile of `librdkafka` by reading the variable "features" on the root of the `confluent-kafka-javascript` object.
+
+```js
+const Kafka = require('@confluentinc/kafka-javascript');
+console.log(Kafka.features);
+
+// #=> [ 'gzip', 'snappy', 'ssl', 'sasl', 'regex', 'lz4' ]
+```
+
+You can also get the version of `librdkafka`
+
+```js
+const Kafka = require('@confluentinc/kafka-javascript');
+console.log(Kafka.librdkafkaVersion);
+
+// #=> 2.3.0
+```
+
+## Usage: promisified API
+
+Still being written. In the meantime, the [QUICKSTART](./QUICKSTART.md) has a good starting point.
+
+## Usage: non-promisified API
+
+### Sending Messages
+
+A `Producer` sends messages to Kafka. The `Producer` constructor takes a configuration object, as shown in the following example:
+
+```js
+const producer = new Kafka.Producer({
+ 'metadata.broker.list': 'kafka-host1:9092,kafka-host2:9092'
+});
+```
+
+A `Producer` requires only `metadata.broker.list` (the Kafka brokers) to be created. The values in this list are separated by commas. For other configuration options, see the [Configuration.md](https://github.com/confluentinc/librdkafka/blob/v2.3.0/CONFIGURATION.md) file described previously.
+
+The following example illustrates a list with several `librdkafka` options set.
+
+```js
+const producer = new Kafka.Producer({
+ 'client.id': 'kafka',
+ 'metadata.broker.list': 'localhost:9092',
+ 'compression.codec': 'gzip',
+ 'retry.backoff.ms': 200,
+ 'message.send.max.retries': 10,
+ 'socket.keepalive.enable': true,
+ 'queue.buffering.max.messages': 100000,
+ 'queue.buffering.max.ms': 1000,
+ 'batch.num.messages': 1000000,
+ 'dr_cb': true
+});
+```
+
+#### Stream API
+
+You can easily use the `Producer` as a writable stream immediately after creation (as shown in the following example):
+
+```js
+// Our producer with its Kafka brokers
+// This call returns a new writable stream to our topic 'topic-name'
+const stream = Kafka.Producer.createWriteStream({
+ 'metadata.broker.list': 'kafka-host1:9092,kafka-host2:9092'
+}, {}, {
+ topic: 'topic-name'
+});
+
+// Writes a message to the stream
+const queuedSuccess = stream.write(Buffer.from('Awesome message'));
+
+if (queuedSuccess) {
+ console.log('We queued our message!');
+} else {
+ // Note that this only tells us if the stream's queue is full,
+ // it does NOT tell us if the message got to Kafka! See below...
+ console.log('Too many messages in our queue already');
+}
+
+// NOTE: MAKE SURE TO LISTEN TO THIS IF YOU WANT THE STREAM TO BE DURABLE
+// Otherwise, any error will bubble up as an uncaught exception.
+stream.on('error', (err) => {
+ // Here's where we'll know if something went wrong sending to Kafka
+ console.error('Error in our kafka stream');
+ console.error(err);
+})
+```
+
+If you do not want your code to crash when an error happens, ensure you have an `error` listener on the stream. Most errors are not necessarily fatal, but the ones that are will immediately destroy the stream. If you use `autoClose`, the stream will close itself at the first sign of a problem.
+
+#### Standard API
+
+The Standard API is more performant, particularly when handling high volumes of messages.
+However, it requires more manual setup to use. The following example illustrates its use:
+
+```js
+const producer = new Kafka.Producer({
+ 'metadata.broker.list': 'localhost:9092',
+ 'dr_cb': true
+});
+
+// Connect to the broker manually
+producer.connect();
+
+// Wait for the ready event before proceeding
+producer.on('ready', () => {
+ try {
+ producer.produce(
+ // Topic to send the message to
+ 'topic',
+ // optionally we can manually specify a partition for the message
+ // this defaults to -1 - which will use librdkafka's default partitioner (consistent random for keyed messages, random for unkeyed messages)
+ null,
+ // Message to send. Must be a buffer
+ Buffer.from('Awesome message'),
+ // for keyed messages, we also specify the key - note that this field is optional
+ 'Stormwind',
+ // you can send a timestamp here. If your broker version supports it,
+ // it will get added. Otherwise, we default to 0
+ Date.now(),
+ // you can send an opaque token here, which gets passed along
+ // to your delivery reports
+ );
+ } catch (err) {
+ console.error('A problem occurred when sending our message');
+ console.error(err);
+ }
+});
+
+// Any errors we encounter, including connection errors
+producer.on('event.error', (err) => {
+ console.error('Error from producer');
+ console.error(err);
+})
+
+// We must either call .poll() manually after sending messages
+// or set the producer to poll on an interval (.setPollInterval).
+// Without this, we do not get delivery events and the queue
+// will eventually fill up.
+producer.setPollInterval(100);
+```
+
+To see the configuration options available to you, see the [Configuration](#configuration) section.
+
+##### Methods
+
+| Method | Description |
+|---------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `producer.connect()` | Connects to the broker.
The `connect()` method emits the `ready` event when it connects successfully. If it does not, the error will be passed through the callback. |
+| `producer.disconnect()` | Disconnects from the broker.
The `disconnect()` method emits the `disconnected` event when it has disconnected. If it does not, the error will be passed through the callback. |
+| `producer.poll()` | Polls the producer for delivery reports or other events to be transmitted via the emitter.
In order to get the events in `librdkafka`'s queue to emit, you must call this regularly. |
+| `producer.setPollInterval(interval)` | Polls the producer on this interval, handling disconnections and reconnection. Set it to 0 to turn it off. |
+| `producer.produce(topic, partition, msg, key, timestamp, opaque)` | Sends a message.
The `produce()` method throws when produce would return an error. Ordinarily, this is just if the queue is full. |
+| `producer.flush(timeout, callback)` | Flush the librdkafka internal queue, sending all messages. Default timeout is 500ms |
+| `producer.initTransactions(timeout, callback)` | Initializes the transactional producer. |
+| `producer.beginTransaction(callback)` | Starts a new transaction. |
+| `producer.sendOffsetsToTransaction(offsets, consumer, timeout, callback)` | Sends consumed topic-partition-offsets to the broker, which will get committed along with the transaction. |
+| `producer.abortTransaction(timeout, callback)` | Aborts the ongoing transaction. |
+| `producer.commitTransaction(timeout, callback)` | Commits the ongoing transaction. |
+
+##### Events
+
+Some configuration properties that end in `_cb` indicate that an event should be generated for that option. You can either:
+
+* provide a value of `true` and react to the event
+* provide a callback function directly
+
+The following example illustrates an event:
+
+```js
+const producer = new Kafka.Producer({
+ 'client.id': 'my-client', // Specifies an identifier to use to help trace activity in Kafka
+ 'metadata.broker.list': 'localhost:9092', // Connect to a Kafka instance on localhost
+ 'dr_cb': true // Specifies that we want a delivery-report event to be generated
+});
+
+// Poll for events every 100 ms
+producer.setPollInterval(100);
+
+producer.on('delivery-report', (err, report) => {
+ // Report of delivery statistics here:
+ //
+ console.log(report);
+});
+```
+
+The following table describes types of events.
+
+| Event | Description |
+|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `disconnected` | The `disconnected` event is emitted when the broker has disconnected.
This event is emitted only when `.disconnect` is called. The wrapper will always try to reconnect otherwise. |
+| `ready` | The `ready` event is emitted when the `Producer` is ready to send messages. |
+| `event` | The `event` event is emitted when `librdkafka` reports an event (if you opted in via the `event_cb` option). |
+| `event.log` | The `event.log` event is emitted when logging events come in (if you opted into logging via the `event_cb` option).
You will need to set a value for `debug` if you want to send information. |
+| `event.stats` | The `event.stats` event is emitted when `librdkafka` reports stats (if you opted in by setting the `statistics.interval.ms` to a non-zero value). |
+| `event.error` | The `event.error` event is emitted when `librdkafka` reports an error |
+| `event.throttle` | The `event.throttle` event emitted when `librdkafka` reports throttling. |
+| `delivery-report` | The `delivery-report` event is emitted when a delivery report has been found via polling.
To use this event, you must set `request.required.acks` to `1` or `-1` in topic configuration and `dr_cb` (or `dr_msg_cb` if you want the report to contain the message payload) to `true` in the `Producer` constructor options. |
+
+#### Higher Level Producer
+
+The higher level producer is a variant of the producer which can propagate callbacks to you upon message delivery.
+
+```js
+const producer = new Kafka.HighLevelProducer({
+ 'metadata.broker.list': 'localhost:9092',
+});
+```
+
+This will enrich the produce call so it will have a callback to tell you when the message has been delivered. You lose the ability to specify opaque tokens.
+
+```js
+producer.produce(topicName, null, Buffer.from('alliance4ever'), null, Date.now(), (err, offset) => {
+ // The offset if our acknowledgement level allows us to receive delivery offsets
+ console.log(offset);
+});
+```
+
+Additionally you can add serializers to modify the value of a produce for a key or value before it is sent over to Kafka.
+
+```js
+producer.setValueSerializer((value) => {
+ return Buffer.from(JSON.stringify(value));
+});
+```
+
+Otherwise the behavior of the class should be exactly the same.
+
+### Kafka.KafkaConsumer
+
+To read messages from Kafka, you use a `KafkaConsumer`. You instantiate a `KafkaConsumer` object as follows:
+
+```js
+const consumer = new Kafka.KafkaConsumer({
+ 'group.id': 'kafka',
+ 'metadata.broker.list': 'localhost:9092',
+}, {});
+```
+
+The first parameter is the global config, while the second parameter is the topic config that gets applied to all subscribed topics. To view a list of all supported configuration properties, see the [Configuration.md](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md) file described previously. Look for the `C` and `*` keys.
+
+The `group.id` and `metadata.broker.list` properties are required for a consumer.
+
+#### Rebalancing
+
+Rebalancing is managed internally by `librdkafka` by default. If you would like to override this functionality, you may provide your own logic as a rebalance callback.
+
+```js
+const consumer = new Kafka.KafkaConsumer({
+ 'group.id': 'kafka',
+ 'metadata.broker.list': 'localhost:9092',
+ 'rebalance_cb': (err, assignment) => {
+
+ if (err.code === Kafka.CODES.ERRORS.ERR__ASSIGN_PARTITIONS) {
+ // Note: this can throw when you are disconnected. Take care and wrap it in
+ // a try catch if that matters to you
+ this.assign(assignment);
+ } else if (err.code == Kafka.CODES.ERRORS.ERR__REVOKE_PARTITIONS){
+ // Same as above
+ this.unassign();
+ } else {
+ // We had a real error
+ console.error(err);
+ }
+
+ }
+})
+```
+
+`this` is bound to the `KafkaConsumer` you have created. By specifying a `rebalance_cb` you can also listen to the `rebalance` event as an emitted event. This event is not emitted when using the internal `librdkafka` rebalancer.
+
+#### Commits
+
+When you commit in `confluent-kafka-javascript`, the standard way is to queue the commit request up with the next `librdkafka` request to the broker. When doing this, there isn't a way to know the result of the commit. Luckily there is another callback you can listen to to get this information
+
+```js
+const consumer = new Kafka.KafkaConsumer({
+ 'group.id': 'kafka',
+ 'metadata.broker.list': 'localhost:9092',
+ 'offset_commit_cb': (err, topicPartitions) => {
+
+ if (err) {
+ // There was an error committing
+ console.error(err);
+ } else {
+ // Commit went through. Let's log the topic partitions
+ console.log(topicPartitions);
+ }
+
+ }
+})
+```
+
+`this` is bound to the `KafkaConsumer` you have created. By specifying an `offset_commit_cb` you can also listen to the `offset.commit` event as an emitted event. It receives an error and the list of topic partitions as argument. This is not emitted unless opted in.
+
+#### Message Structure
+
+Messages that are returned by the `KafkaConsumer` have the following structure.
+
+```js
+{
+ value: Buffer.from('hi'), // message contents as a Buffer
+ size: 2, // size of the message, in bytes
+ topic: 'librdtesting-01', // topic the message comes from
+ offset: 1337, // offset the message was read from
+ partition: 1, // partition the message was on
+ key: 'someKey', // key of the message if present
+ timestamp: 1510325354780 // timestamp of message creation
+}
+```
+
+#### Stream API
+
+The stream API is the easiest way to consume messages. The following example illustrates the use of the stream API:
+
+```js
+// Read from the librdtesting-01 topic... note that this creates a new stream on each call!
+const stream = KafkaConsumer.createReadStream(globalConfig, topicConfig, {
+ topics: ['librdtesting-01']
+});
+
+stream.on('data', (message) => {
+ console.log('Got message');
+ console.log(message.value.toString());
+});
+```
+
+You can also get the `consumer` from the streamConsumer, for using consumer methods. The following example illustrates that:
+
+```js
+stream.consumer.commit(); // Commits all locally stored offsets
+```
+
+#### Standard API
+
+You can also use the Standard API and manage callbacks and events yourself. You can choose different modes for consuming messages:
+
+* *Flowing mode*. This mode flows all of the messages it can read by maintaining an infinite loop in the event loop. It only stops when it detects the consumer has issued the `unsubscribe` or `disconnect` method.
+* *Non-flowing mode*. This mode reads a single message from Kafka at a time manually.
+
+The following example illustrates flowing mode:
+```js
+// Flowing mode
+consumer.connect();
+
+consumer
+ .on('ready', () => {
+ consumer.subscribe(['librdtesting-01']);
+
+ // Consume from the librdtesting-01 topic. This is what determines
+ // the mode we are running in. By not specifying a callback (or specifying
+ // only a callback) we get messages as soon as they are available.
+ consumer.consume();
+ })
+ .on('data', (data) => {
+ // Output the actual message contents
+ console.log(data.value.toString());
+ });
+```
+The following example illustrates non-flowing mode:
+```js
+// Non-flowing mode
+consumer.connect();
+
+consumer
+ .on('ready', () => {
+ // Subscribe to the librdtesting-01 topic
+ // This makes subsequent consumes read from that topic.
+ consumer.subscribe(['librdtesting-01']);
+
+ // Read one message every 1000 milliseconds
+ setInterval(() => {
+ consumer.consume(1);
+ }, 1000);
+ })
+ .on('data', (data) => {
+ console.log('Message found! Contents below.');
+ console.log(data.value.toString());
+ });
+```
+
+The following table lists important methods for this API.
+
+| Method | Description |
+|-----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `consumer.connect()` | Connects to the broker.
The `connect()` emits the event `ready` when it has successfully connected. If it does not, the error will be passed through the callback. |
+| `consumer.disconnect()` | Disconnects from the broker.
The `disconnect()` method emits `disconnected` when it has disconnected. If it does not, the error will be passed through the callback. |
+| `consumer.subscribe(topics)` | Subscribes to an array of topics. |
+| `consumer.unsubscribe()` | Unsubscribes from the currently subscribed topics.
You cannot subscribe to different topics without calling the `unsubscribe()` method first. |
+| `consumer.consume(cb)` | Gets messages from the existing subscription as quickly as possible. If `cb` is specified, invokes `cb(err, message)`.
This method keeps a background thread running to do the work. Note that the number of threads in nodejs process is limited by `UV_THREADPOOL_SIZE` (default value is 4) and using up all of them blocks other parts of the application that need threads. If you need multiple consumers then consider increasing `UV_THREADPOOL_SIZE` or using `consumer.consume(number, cb)` instead. |
+| `consumer.consume(number, cb)` | Gets `number` of messages from the existing subscription. If `cb` is specified, invokes `cb(err, message)`. |
+| `consumer.commit()` | Commits all locally stored offsets |
+| `consumer.commit(topicPartition)` | Commits offsets specified by the topic partition |
+| `consumer.commitMessage(message)` | Commits the offsets specified by the message |
+
+The following table lists events for this API.
+
+| Event | Description |
+|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `data` | When using the Standard API consumed messages are emitted in this event. |
+| `partition.eof` | When using Standard API and the configuration option `enable.partition.eof` is set, `partition.eof` events are emitted in this event. The event contains `topic`, `partition` and `offset` properties. |
+| `warning` | The event is emitted in case of `UNKNOWN_TOPIC_OR_PART` or `TOPIC_AUTHORIZATION_FAILED` errors when consuming in *Flowing mode*. Since the consumer will continue working if the error is still happening, the warning event should reappear after the next metadata refresh. To control the metadata refresh rate set `topic.metadata.refresh.interval.ms` property. Once you resolve the error, you can manually call `getMetadata` to speed up consumer recovery. |
+| `disconnected` | The `disconnected` event is emitted when the broker disconnects.
This event is only emitted when `.disconnect` is called. The wrapper will always try to reconnect otherwise. |
+| `ready` | The `ready` event is emitted when the `Consumer` is ready to read messages. |
+| `event` | The `event` event is emitted when `librdkafka` reports an event (if you opted in via the `event_cb` option). |
+| `event.log` | The `event.log` event is emitted when logging events occur (if you opted in for logging via the `event_cb` option).
You will need to set a value for `debug` if you want information to send. |
+| `event.stats` | The `event.stats` event is emitted when `librdkafka` reports stats (if you opted in by setting the `statistics.interval.ms` to a non-zero value). |
+| `event.error` | The `event.error` event is emitted when `librdkafka` reports an error |
+| `event.throttle` | The `event.throttle` event is emitted when `librdkafka` reports throttling. |
+
+### Reading current offsets from the broker for a topic
+
+Some times you find yourself in the situation where you need to know the latest (and earliest) offset for one of your topics. Connected producers and consumers both allow you to query for these through `queryWaterMarkOffsets` like follows:
+
+```js
+const timeout = 5000, partition = 0;
+consumer.queryWatermarkOffsets('my-topic', partition, timeout, (err, offsets) => {
+ const high = offsets.highOffset;
+ const low = offsets.lowOffset;
+});
+
+producer.queryWatermarkOffsets('my-topic', partition, timeout, (err, offsets) => {
+ const high = offsets.highOffset;
+ const low = offsets.lowOffset;
+});
+
+An error will be returned if the client was not connected or the request timed out within the specified interval.
+
+```
+
+### Metadata
+
+Both `Kafka.Producer` and `Kafka.KafkaConsumer` include a `getMetadata` method to retrieve metadata from Kafka.
+
+Getting metadata on any connection returns the following data structure:
+
+```js
+{
+ orig_broker_id: 1,
+ orig_broker_name: "broker_name",
+ brokers: [
+ {
+ id: 1,
+ host: 'localhost',
+ port: 40
+ }
+ ],
+ topics: [
+ {
+ name: 'awesome-topic',
+ partitions: [
+ {
+ id: 1,
+ leader: 20,
+ replicas: [1, 2],
+ isrs: [1, 2]
+ }
+ ]
+ }
+ ]
+}
+```
+
+The following example illustrates how to use the `getMetadata` method.
+
+When fetching metadata for a specific topic, if a topic reference does not exist, one is created using the default config.
+Please see the documentation on `Client.getMetadata` if you want to set configuration parameters, e.g. `acks`, on a topic to produce messages to.
+
+```js
+const opts = {
+ topic: 'librdtesting-01',
+ timeout: 10000
+};
+
+producer.getMetadata(opts, (err, metadata) => {
+ if (err) {
+ console.error('Error getting metadata');
+ console.error(err);
+ } else {
+ console.log('Got metadata');
+ console.log(metadata);
+ }
+});
+```
+
+### Admin Client
+
+`confluent-kafka-javascript` now supports the admin client for creating, deleting, and scaling out topics. The `librdkafka` APIs also support altering configuration of topics and broker, but that is not currently implemented.
+
+To create an Admin client, you can do as follows:
+
+```js
+const Kafka = require('@confluentinc/kafka-javascript');
+
+const client = Kafka.AdminClient.create({
+ 'client.id': 'kafka-admin',
+ 'metadata.broker.list': 'broker01'
+});
+```
+
+This will instantiate the `AdminClient`, which will allow the calling of the admin methods.
+
+```js
+client.createTopic({
+ topic: topicName,
+ num_partitions: 1,
+ replication_factor: 1
+}, (err) => {
+ // Done!
+});
+```
+
+All of the admin api methods can have an optional timeout as their penultimate parameter.
+
+The following table lists important methods for this API.
+
+| Method | Description |
+|----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------|
+| `client.disconnect()` | Destroy the admin client, making it invalid for further use. |
+| `client.createTopic(topic, timeout, cb)` | Create a topic on the broker with the given configuration. See JS doc for more on structure of the topic object |
+| `client.deleteTopic(topicName, timeout, cb)` | Delete a topic of the given name |
+| `client.createPartitions(topicName, desiredPartitions, timeout, cb)` | Create partitions until the topic has the desired number of partitions. |
+
+Check the tests for an example of how to use this API!
diff --git a/LICENSE.kafkajs b/LICENSE.kafkajs
new file mode 100644
index 00000000..3a91a6dc
--- /dev/null
+++ b/LICENSE.kafkajs
@@ -0,0 +1,31 @@
+The promisified API (lib/kafkajs) is inspired by kafkajs (github.com/tulios/kafkajs).
+The promisified tests (test/promisified) are also adapted from there.
+Many error types are also adapted from there.
+The license notice is reproduced below.
+
+----
+
+The MIT License
+
+Copyright (c) 2018 Túlio Ornelas (ornelas.tulio@gmail.com)
+
+Permission is hereby granted, free of charge,
+to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to
+deal in the Software without restriction, including
+without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom
+the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
+ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/LICENSE.node-rdkafka b/LICENSE.node-rdkafka
new file mode 100644
index 00000000..e7a18a6f
--- /dev/null
+++ b/LICENSE.node-rdkafka
@@ -0,0 +1,25 @@
+This project is based on node-rdkafka (github.com/Blizzard/node-rdkafka).
+The license notice is reproduced below.
+
+--------
+
+The MIT License (MIT)
+Copyright (c) 2016 Blizzard Entertainment
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
index d5ad6d41..101cc5a7 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,5 +1,5 @@
The MIT License (MIT)
-Copyright (c) 2016-2023 Blizzard Entertainment
+Copyright (c) 2023 Confluent, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/MIGRATION.md b/MIGRATION.md
new file mode 100644
index 00000000..3c28761b
--- /dev/null
+++ b/MIGRATION.md
@@ -0,0 +1,406 @@
+# Migration Guide
+
+## KafkaJS
+
+
+1. Change the import statement, and add a `kafkaJS` block around your configs.
+ ```javascript
+ const { Kafka } = require('kafkajs');
+ const kafka = new Kafka({ brokers: ['kafka1:9092', 'kafka2:9092'], /* ... */ });
+ const producer = kafka.producer({ /* ... */, });
+ ```
+ to
+ ```javascript
+ const { Kafka } = require('@confluentinc/kafka-javascript').KafkaJS;
+ const kafka = new Kafka({ kafkaJS: { brokers: ['kafka1:9092', 'kafka2:9092'], /* ... */ } });
+ const producer = kafka.producer({ kafkaJS: { /* ... */, } });
+ ```
+
+2. Try running your program. In case a migration is needed, an informative error will be thrown.
+ If you're using Typescript, some of these changes will be caught at compile time.
+
+3. The most common expected changes to the code are:
+ - For the **producer**: `acks`, `compression` and `timeout` are not set on a per-send() basis.
+ Rather, they must be configured in the top-level configuration while creating the producer.
+ - For the **consumer**:
+ - `fromBeginning` is not set on a per-subscribe() basis.
+ Rather, it must be configured in the top-level configuration while creating the consumer.
+ - `autoCommit` and `autoCommitInterval` are not set on a per-run() basis.
+ Rather, they must be configured in the top-level configuration while creating the consumer.
+ - `autoCommitThreshold` is not supported.
+ - `eachBatch`'s batch size never exceeds 1.
+ - For errors: Check the `error.code` rather than the error `name` or `type`.
+
+4. A more exhaustive list of semantic and configuration differences is [presented below](#common).
+
+5. An example migration:
+
+```diff
+-const { Kafka } = require('kafkajs');
++const { Kafka } = require('@confluentinc/kafka-javascript').KafkaJS;
+
+const kafka = new Kafka({
++ kafkaJS: {
+ clientId: 'my-app',
+ brokers: ['kafka1:9092', 'kafka2:9092']
++ }
+})
+
+const producerRun = async () => {
+- const producer = kafka.producer();
++ const producer = kafka.producer({ kafkaJS: { acks: 1 } });
+ await producer.connect();
+ await producer.send({
+ topic: 'test-topic',
+- acks: 1,
+ messages: [
+ { value: 'Hello confluent-kafka-javascript user!' },
+ ],
+ });
+};
+
+
+const consumerRun = async () => {
+ // Consuming
+- const consumer = kafka.consumer({ groupId: 'test-group' });
++ const consumer = kafka.consumer({ kafkaJS: { groupId: 'test-group', fromBeginning: true } });
+ await consumer.connect();
+- await consumer.subscribe({ topic: 'test-topic', fromBeginning: true });
++ await consumer.subscribe({ topic: 'test-topic' });
+
+ await consumer.run({
+ eachMessage: async ({ topic, partition, message }) => {
+ console.log({
+ partition,
+ offset: message.offset,
+ value: message.value.toString(),
+ })
+ },
+ });
+};
+
+producerRun().then(consumerRun).catch(console.error);
+```
+
+### Common
+
+#### Configuration changes
+ ```javascript
+ const kafka = new Kafka({ kafkaJS: { /* common configuration changes */ } });
+ ```
+ Each allowed config property is discussed in the table below.
+ If there is any change in semantics or the default values, the property and the change is **highlighted in bold**.
+
+ | Property | Default Value | Comment |
+ |-------------------------------|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+ | **brokers** | - | A list of strings, representing the bootstrap brokers. **A function is no longer allowed as an argument for this.** |
+ | **ssl** | false | A boolean, set to true if ssl needs to be enabled. **Additional properties like CA, certificate, key, etc. need to be specified outside the kafkaJS block.** |
+ | **sasl** | - | An optional object of the form `{ mechanism: 'plain' | 'scram-sha-256' | 'scram-sha-512', username: string, password: string }` or `{ mechanism: 'oauthbearer', oauthBearerProvider: function }`. Note that for OAUTHBEARER based authentication, the provider function must return lifetime (in ms), and principal name along with token value. **Additional authentication types are not supported.** |
+ | clientId | "rdkafka" | An optional string used to identify the client. |
+ | **connectionTimeout** | 1000 | This timeout is not enforced individually, but a sum of `connectionTimeout` and `authenticationTimeout` is enforced together. |
+ | **authenticationTimeout** | 10000 | This timeout is not enforced individually, but a sum of `connectionTimeout` and `authenticationTimeout` is enforced together. |
+ | **reauthenticationThreshold** | **80% of connections.max.reauth.ms** | **No longer checked, the default is always used.** |
+ | requestTimeout | 30000 | number of milliseconds for a network request to timeout. |
+ | **enforceRequestTimeout** | true | When set to false, `requestTimeout` is set to 5 minutes. **This cannot be completely disabled.** |
+ | retry | object | Properties individually discussed below. |
+ | retry.maxRetryTime | 30000 | maximum time to backoff a retry, in milliseconds. |
+ | retry.initialRetryTime | 300 | minimum time to backoff a retry, in milliseconds |
+ | **retry.retries** | 5 | Total cap on the number of retries. **Applicable only to Produce requests.** |
+ | **retry.factor** | 0.2 | Randomization factor (jitter) for backoff. **Cannot be changed**. |
+ | **retry.multiplier** | 2 | Multiplier for exponential factor of backoff. **Cannot be changed.** |
+ | **retry.restartOnFailure** | true | Consumer only. **Cannot be changed**. Consumer will always make an attempt to restart. |
+ | logLevel | `logLevel.INFO` | Decides the severity level of the logger created by the underlying library. A logger created with the `INFO` level will not be able to log `DEBUG` messages later. |
+ | **socketFactory** | null | **No longer supported.** |
+ | outer config | {} | The configuration outside the kafkaJS block can contain any of the keys present in the [librdkafka CONFIGURATION table](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md). |
+
+
+### Producer
+
+#### Producer Configuration Changes
+
+ ```javascript
+ const producer = kafka.producer({ kafkaJS: { /* producer-specific configuration changes. */ } });
+ ```
+
+ Each allowed config property is discussed in the table below.
+ If there is any change in semantics or the default values, the property and the change is **highlighted in bold**.
+
+ | Property | Default Value | Comment |
+ |-------------------------|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+ | **createPartitioner** | DefaultPartioner (murmur2_random) - Java client compatible | Custom partitioner support is not yet provided. The default partitioner's behaviour is retained, and a number of partitioners are provided via the `partitioner` property, which is specified outside the `kafkaJS` block. |
+ | **retry** | object | Identical to `retry` in the common configuration. This takes precedence over the common config retry. |
+ | metadataMaxAge | 5 minutes | Time in milliseconds after which to refresh metadata for known topics |
+ | allowAutoTopicCreation | true | Determines if a topic should be created if it doesn't exist while producing. |
+ | transactionTimeout | 60000 | The maximum amount of time in milliseconds that the transaction coordinator will wait for a transaction status update from the producer before proactively aborting the ongoing transaction. Only applicable when `transactionalId` is set to true. |
+ | idempotent | false | If set to true, ensures that messages are delivered exactly once and in order. If true, certain constraints must be respected for other properties, `maxInFlightRequests <= 5`, `retry.retries >= 0` |
+ | **maxInFlightRequests** | null | Maximum number of in-flight requests **per broker connection**. If not set, it is practically unbounded (same as KafkaJS). |
+ | transactionalId | null | If set, turns this into a transactional producer with this identifier. This also automatically sets `idempotent` to true. |
+ | **acks** | -1 | The number of required acks before a Produce succeeds. **This is set on a per-producer level, not on a per `send` level**. -1 denotes it will wait for all brokers in the in-sync replica set. |
+ | **compression** | CompressionTypes.NONE | Compression codec for Produce messages. **This is set on a per-producer level, not on a per `send` level**. It must be a key of CompressionType, namely GZIP, SNAPPY, LZ4, ZSTD or NONE. |
+ | **timeout** | 30000 | The ack timeout of the producer request in milliseconds. This value is only enforced by the broker. **This is set on a per-producer level, not on a per `send` level**. |
+ | outer config | {} | The configuration outside the kafkaJS block can contain any of the keys present in the [librdkafka CONFIGURATION table](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md). |
+
+
+
+#### Semantic and Per-Method Changes
+
+* `send`: and `sendBatch`:`
+ - While sending multiple messages, even if one of the messages fails, the method throws an error.
+ - While `sendBatch` is available, it acts as a wrapper around send, and the actual batching is handled by librdkafka.
+ - `acks`, `compression` and `timeout` are not set on a per-send basis. Rather, they must be configured in the top-level configuration. See [configuration changes](#producer-configuration-changes).
+ Additionally, there are several more compression types available by default besides GZIP.
+ Before:
+ ```javascript
+ const kafka = new Kafka({/* ... */});
+ const producer = kafka.producer();
+ await producer.connect();
+
+ await producer.send({
+ topic: 'test',
+ messages: [ /* ... */ ],
+ acks: 1,
+ compression: CompressionTypes.GZIP,
+ timeout: 30000,
+ });
+ ```
+
+ After:
+ ```javascript
+ const kafka = new Kafka({ kafkaJS: { /* ... */ }});
+ const producer = kafka.producer({
+ kafkaJS: {
+ acks: 1,
+ compression: CompressionTypes.GZIP|CompressionTypes.SNAPPY|CompressionTypes.LZ4|CompressionTypes.ZSTD|CompressionTypes.NONE,
+ timeout: 30000,
+ }
+ });
+ await producer.connect();
+
+ await producer.send({
+ topic: 'test',
+ messages: [ /* ... */ ],
+ });
+ ```
+ - It's recommended to send a number of messages without awaiting them, and then calling `flush` to ensure all messages are sent, rather than awaiting each message. This is more efficient.
+ Example:
+ ```javascript
+ const kafka = new Kafka({ kafkaJS: { /* ... */ }});
+ const producer = kafka.producer();
+ await producer.connect();
+ for (/*...*/) producer.send({ /* ... */});
+ await producer.flush({timeout: 5000});
+ ```
+
+ However, in case it is desired to await every message, `linger.ms` should be set to 0, to ensure that the default batching behaviour does not cause a delay in awaiting messages.
+ Example:
+ ```javascript
+ const kafka = new Kafka({ kafkaJS: { /* ... */ }});
+ const producer = kafka.producer({ 'linger.ms': 0 });
+ ```
+
+* A transactional producer (with a `transactionId`) set, **cannot** send messages without initiating a transaction using `producer.transaction()`.
+* While using `sendOffsets` from a transactional producer, the `consumerGroupId` argument must be omitted, and rather, the consumer object itself must be passed instead.
+
+### Consumer
+
+#### Consumer Configuration Changes
+
+ ```javascript
+ const consumer = kafka.consumer({ kafkaJS: { /* producer-specific configuration changes. */ } });
+ ```
+ Each allowed config property is discussed in the table below.
+ If there is any change in semantics or the default values, the property and the change is **highlighted in bold**.
+
+ | Property | Default Value | Comment |
+ |--------------------------|-----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+ | groupId | - | A mandatory string denoting consumer group name that this consumer is a part of. |
+ | **partitionAssigners** | `[PartitionAssigners.roundRobin]` | Support for range, roundRobin, and cooperativeSticky assignors is provided. Custom assignors are not supported. |
+ | **partitionAssignors** | `[PartitionAssignors.roundRobin]` | Alias for `partitionAssigners` |
+ | **rebalanceTimeout** | **300000** | The maximum allowed time for each member to join the group once a rebalance has begun. Note, that setting this value *also* changes the max poll interval. Message processing in `eachMessage/eachBatch` must not take more than this time. |
+ | heartbeatInterval | 3000 | The expected time in milliseconds between heartbeats to the consumer coordinator. |
+ | metadataMaxAge | 5 minutes | Time in milliseconds after which to refresh metadata for known topics |
+ | allowAutoTopicCreation | true | Determines if a topic should be created if it doesn't exist while consuming. |
+ | **maxBytesPerPartition** | 1048576 (1MB) | determines how many bytes can be fetched in one request from a single partition. There is a change in semantics, this size grows dynamically if a single message larger than this is encountered, and the client does not get stuck. |
+ | minBytes | 1 | Minimum number of bytes the broker responds with (or wait until `maxWaitTimeInMs`) |
+ | maxBytes | 10485760 (10MB) | Maximum number of bytes the broker responds with. |
+ | **retry** | object | Identical to `retry` in the common configuration. This takes precedence over the common config retry. |
+ | readUncommitted | false | If true, consumer will read transactional messages which have not been committed. |
+ | **maxInFlightRequests** | null | Maximum number of in-flight requests **per broker connection**. If not set, it is practically unbounded (same as KafkaJS). |
+ | rackId | null | Can be set to an arbitrary string which will be used for fetch-from-follower if set up on the cluster. |
+ | **fromBeginning** | false | If there is initial offset in offset store or the desired offset is out of range, and this is true, we consume the earliest possible offset. **This is set on a per-consumer level, not on a per `subscribe` level**. |
+ | **autoCommit** | true | Whether to periodically auto-commit offsets to the broker while consuming. **This is set on a per-consumer level, not on a per `run` level**. |
+ | **autoCommitInterval** | 5000 | Offsets are committed periodically at this interval, if autoCommit is true. **This is set on a per-consumer level, not on a per `run` level. The default value is changed to 5 seconds.**. |
+ | outer config | {} | The configuration outside the kafkaJS block can contain any of the keys present in the [librdkafka CONFIGURATION table](https://github.com/confluentinc/librdkafka/blob/master/CONFIGURATION.md). |
+
+
+#### Semantic and Per-Method Changes
+
+
+* `subscribe`:
+ - Regex flags are ignored while passing a topic subscription (like 'i' or 'g'). Regexes must start with '^', otherwise, an error is thrown.
+ - Subscribe must be called only after `connect`.
+ - An optional parameter, `replace` is provided.
+ If set to true, the current subscription is replaced with the new one. If set to false, the new subscription is added to the current one, for example,
+ `consumer.subscribe({ topics: ['topic1'], replace: true});`.
+ The default value is false to retain existing behaviour.
+ - While passing a list of topics to `subscribe`, the `fromBeginning` is not set on a per-subscribe basis. Rather, it must be configured in the top-level configuration.
+
+ Before:
+ ```javascript
+ const consumer = kafka.consumer({
+ groupId: 'test-group',
+ });
+ await consumer.connect();
+ await consumer.subscribe({ topics: ["topic"], fromBeginning: true});
+ ```
+ After:
+ ```javascript
+ const consumer = kafka.consumer({
+ kafkaJS: {
+ groupId: 'test-group',
+ fromBeginning: true,
+ }
+ });
+ await consumer.connect();
+ await consumer.subscribe({ topics: ["topic"] });
+ ```
+
+* `run` :
+ - For auto-committing using a consumer, the properties `autoCommit` and `autoCommitInterval` on `run` are not set on a per-subscribe basis.
+ Rather, they must be configured in the top-level configuration.
+ `autoCommitThreshold` is not supported.
+ If `autoCommit` is set to true, messages are *not* committed per-message, but rather periodically at the interval specified by `autoCommitInterval` (default 5 seconds).
+
+ Before:
+ ```javascript
+ const kafka = new Kafka({ /* ... */ });
+ const consumer = kafka.consumer({ /* ... */ });
+ await consumer.connect();
+ await consumer.subscribe({ topics: ["topic"] });
+ consumer.run({
+ eachMessage: someFunc,
+ autoCommit: true,
+ autoCommitInterval: 5000,
+ });
+ ```
+ After:
+ ```javascript
+ const kafka = new Kafka({ kafkaJS: { /* ... */ } });
+ const consumer = kafka.consumer({
+ kafkaJS: {
+ /* ... */,
+ autoCommit: true,
+ autoCommitInterval: 5000,
+ },
+ });
+ await consumer.connect();
+ await consumer.subscribe({ topics: ["topic"] });
+ consumer.run({
+ eachMessage: someFunc,
+ });
+ ```
+ - The `heartbeat()` no longer needs to be called by the user in the `eachMessage/eachBatch` callback.
+ Heartbeats are automatically managed by librdkafka.
+ - The `partitionsConsumedConcurrently` is supported by both `eachMessage` and `eachBatch`.
+ - An API compatible version of `eachBatch` is available, but the batch size calculation is not
+ as per configured parameters, rather, a constant maximum size is configured internally. This is subject
+ to change.
+ The property `eachBatchAutoResolve` is supported.
+ Within the `eachBatch` callback, use of `uncommittedOffsets` is unsupported,
+ and within the returned batch, `offsetLag` and `offsetLagLow` are unsupported.
+* `commitOffsets`:
+ - Does not yet support sending metadata for topic partitions being committed.
+ - If called with no arguments, it commits all offsets passed to the user (or the stored offsets, if manually handling offset storage using `consumer.storeOffsets`).
+* `seek`:
+ - The restriction to call seek only after `run` is removed. It can be called any time.
+* `pause` and `resume`:
+ - These methods MUST be called after the consumer group is joined.
+ In practice, this means it can be called whenever `consumer.assignment()` has a non-zero size, or within the `eachMessage/eachBatch` callback.
+* `stop` is not yet supported, and the user must disconnect the consumer.
+
+### Admin Client
+
+The admin-client only has support for a limited subset of methods, with more to be added.
+
+ * The `createTopics` method does not yet support the `validateOnly` or `waitForLeaders` properties, and the per-topic configuration
+ does not support `replicaAssignment`.
+ * The `deleteTopics` method is fully supported.
+ * The `listTopics` method is supported with an additional `timeout` option.
+ * The `listGroups` method is supported with additional `timeout` and `matchConsumerGroupStates` options.
+ A number of additional properties have been added to the returned groups, and a list of errors within the returned object.
+ * The `describeGroups` method is supported with additional `timeout` and `includeAuthorizedOperations` options.
+ A number of additional properties have been added to the returned groups.
+ * The `deleteGroups` method is supported with an additional `timeout` option.
+
+### Using the Schema Registry
+
+In case you are using the Schema Registry client at `kafkajs/confluent-schema-registry`, you will not need to make any changes to the usage.
+An example is made available [here](./examples/kafkajs/sr.js).
+
+### Error Handling
+
+ Convert any checks based on `instanceof` and `error.name` or to error checks based on `error.code` or `error.type`.
+
+ **Example**:
+ ```javascript
+ try {
+ await producer.send(/* args */);
+ } catch (error) {
+ if (!Kafka.isKafkaJSError(error)) { /* unrelated err handling */ }
+ else if (error.fatal) { /* fatal error, abandon producer */ }
+ else if (error.code === Kafka.ErrorCode.ERR__QUEUE_FULL) { /*...*/ }
+ else if (error.type === 'ERR_MSG_SIZE_TOO_LARGE') { /*...*/ }
+ /* and so on for specific errors */
+ }
+ ```
+
+ **Error Type Changes**:
+
+ Some possible subtypes of `KafkaJSError` have been removed,
+ and additional information has been added into `KafkaJSError`.
+ Fields have been added denoting if the error is fatal, retriable, or abortable (the latter two only relevant for a transactional producer).
+ Some error-specific fields have also been removed.
+
+ An exhaustive list of changes is at the bottom of this section.
+
+ For compatibility, as many error types as possible have been retained, but it is
+ better to switch to checking the `error.code`.
+
+
+ Exhaustive list of error types and error fields removed:
+ | Error | Change |
+ |-------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+ | `KafkaJSNonRetriableError` | Removed. Retriable errors are automatically retried by librdkafka, so there's no need for this type. Note that `error.retriable` still exists, but it's applicable only for transactional producer, where users are expected to retry an action themselves. All error types using this as a superclass now use `KafkaJSError` as their superclass. |
+ | `KafkaJSOffsetOutOfRange` | `topic` and `partition` are removed from this object. |
+ | `KafkaJSMemberIdRequired` | Removed. Automatically handled by librdkafka. |
+ | `KafkaJSNumberOfRetriesExceeded` | Removed. Retries are handled by librdkafka. |
+ | `KafkaJSNumberOfRetriesExceeded` | `broker, correlationId, createdAt, sentAt` and `pendingDuration` are removed from this object. |
+ | `KafkaJSMetadataNotLoaded` | Removed. Metadata is automatically reloaded by librdkafka. |
+ | `KafkaJSTopicMetadataNotLoaded` | Removed. Topic metadata is automatically reloaded by librdkafka. |
+ | `KafkaJSStaleTopicMetadataAssignment` | removed as it's automatically refreshed by librdkafka. |
+ | `KafkaJSDeleteGroupsError` | Removed, as the Admin Client doesn't have this yet. May be added back again, or changed. |
+ | `KafkaJSServerDoesNotSupportApiKey` | Removed, as this error isn't generally exposed to user in librdkafka. If raised, it is subsumed into `KafkaJSError` where `error.code === Kafka.ErrorCode.ERR_UNSUPPORTED_VERSION`. |
+ | `KafkaJSBrokerNotFound` | Removed. This error isn't exposed directly to the user in librdkafka. |
+ | `KafkaJSLockTimeout` | Removed. This error is not applicable while using librdkafka. |
+ | `KafkaJSUnsupportedMagicByteInMessageSet` | Removed. It is subsumed into `KafkaJSError` where `error.code === Kafka.ErrorCode.ERR_UNSUPPORTED_VERSION`. |
+ | `KafkaJSDeleteTopicRecordsError` | Removed, as the Admin Client doesn't have this yet. May be added back again, or changed. |
+ | `KafkaJSInvariantViolation` | Removed, as it's not applicable to librdkafka. Errors in internal state are subsumed into `KafkaJSError` where `error.code === Kafka.ErrorCode.ERR__STATE`. |
+ | `KafkaJSInvalidVarIntError` | Removed. This error isn't exposed directly to the user in librdkafka. |
+ | `KafkaJSInvalidLongError` | Removed. This error isn't exposed directly to the user in librdkafka. |
+ | `KafkaJSCreateTopicError` | Removed, as the Admin Client doesn't have this yet. May be added back again, or changed.. |
+ | `KafkaJSAlterPartitionReassignmentsError` | removed, as the RPC is not used in librdkafka. |
+ | `KafkaJSFetcherRebalanceError` | Removed. This error isn't exposed directly to the user in librdkafka. |
+ | `KafkaJSConnectionError` | `broker` is removed from this object. |
+ | `KafkaJSConnectionClosedError` | Removed. Subsumed into `KafkaJSConnectionError` as librdkafka treats them equivalently. |
+
+## node-rdkafka
+
+Change the import statement, from
+ ```javascript
+ const Kafka = require('node-rdkafka');
+ ```
+ to
+ ```javascript
+ const Kafka = require('@confluentinc/kafka-javascript');
+ ```
+The rest of the functionality should work as usual.
\ No newline at end of file
diff --git a/Makefile b/Makefile
index c054b210..e8ab3f5e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,19 +1,14 @@
NODE-GYP ?= node_modules/.bin/node-gyp
-# Sick of changing this. Do a check and try to use python 2 if it doesn't work
-PYTHON_VERSION_FULL := $(wordlist 2,4,$(subst ., ,$(shell python --version 2>&1)))
-PYTHON_VERSION_MAJOR := $(word 1,${PYTHON_VERSION_FULL})
-
-ifeq ($(PYTHON_VERSION_MAJOR), 2)
PYTHON = python
-else
-PYTHON = python2
+ifeq (, $(shell command -v python))
+ PYTHON = python3
endif
-
NODE ?= node
CPPLINT ?= cpplint.py
BUILDTYPE ?= Release
-TESTS = "test/**/*.js"
+TESTS = $(ls test/producer/*.js test/*.js test/tools/*.js)
+PROMISIFIED_TESTS = "test/promisified"
E2E_TESTS = $(wildcard e2e/*.spec.js)
TEST_REPORTER =
TEST_OUTPUT =
@@ -23,8 +18,7 @@ CONFIG_OUTPUTS = \
build/binding.Makefile build/config.gypi
CPPLINT_FILES = $(wildcard src/*.cc src/*.h)
-CPPLINT_FILTER = -legal/copyright
-JSLINT_FILES = lib/*.js test/*.js e2e/*.js
+CPPLINT_FILTER = -legal/copyright,-readability/todo,-whitespace/indent_namespace,-runtime/references
PACKAGE = $(shell node -pe 'require("./package.json").name.split("/")[1]')
VERSION = $(shell node -pe 'require("./package.json").version')
@@ -38,13 +32,13 @@ endif
all: lint lib test e2e
-lint: cpplint jslint
+lint: cpplint eslint
cpplint:
@$(PYTHON) $(CPPLINT) --filter=$(CPPLINT_FILTER) $(CPPLINT_FILES)
-jslint: node_modules/.dirstamp
- @./node_modules/.bin/jshint --verbose $(JSLINT_FILES)
+eslint: node_modules/.dirstamp
+ @./node_modules/.bin/eslint .
lib: node_modules/.dirstamp $(CONFIG_OUTPUTS)
@PYTHONHTTPSVERIFY=0 $(NODE-GYP) build $(GYPBUILDARGS)
@@ -59,6 +53,9 @@ $(CONFIG_OUTPUTS): node_modules/.dirstamp binding.gyp
test: node_modules/.dirstamp
@./node_modules/.bin/mocha --ui exports $(TEST_REPORTER) $(TESTS) $(TEST_OUTPUT)
+promisified_test: node_modules/.dirstamp
+ @./node_modules/.bin/jest --ci --runInBand $(PROMISIFIED_TESTS)
+
check: node_modules/.dirstamp
@$(NODE) util/test-compile.js
@@ -78,7 +75,7 @@ endef
docs: node_modules/.dirstamp
@rm -rf docs
- @./node_modules/jsdoc/jsdoc.js --destination docs \
+ @./node_modules/jsdoc/jsdoc.js --debug --destination docs \
--recurse -R ./README.md \
-c ./jsdoc.conf \
--tutorials examples/ ./lib
@@ -91,4 +88,4 @@ release-patch:
clean: node_modules/.dirstamp
@rm -f deps/librdkafka/config.h
- @$(NODE-GYP) clean
+ @$(NODE-GYP) clean
\ No newline at end of file
diff --git a/QUICKSTART.md b/QUICKSTART.md
new file mode 100644
index 00000000..1dddd66b
--- /dev/null
+++ b/QUICKSTART.md
@@ -0,0 +1,70 @@
+# Basic Producer Example
+
+```javascript
+const { Kafka } = require('@confluentinc/kafka-javascript').KafkaJS;
+
+async function producerStart() {
+ const producer = new Kafka().producer({
+ 'bootstrap.servers': '',
+ });
+
+ await producer.connect();
+
+ const deliveryReports = await producer.send({
+ topic: 'topic2',
+ messages: [
+ { value: 'v222', partition: 0 },
+ { value: 'v11', partition: 0, key: 'x' },
+ ]
+ });
+
+ await producer.disconnect();
+}
+
+producerStart();
+```
+
+# Basic Consumer Example
+
+```javascript
+const { Kafka } = require('@confluentinc/kafka-javascript').KafkaJS;
+
+async function consumerStart() {
+ let consumer;
+ let stopped = false;
+
+ // Initialization
+ consumer = new Kafka().consumer({
+ 'bootstrap.servers': '',
+ 'group.id': 'test',
+ 'auto.offset.reset': 'earliest',
+ });
+
+ await consumer.connect();
+ await consumer.subscribe({ topics: ["topic"] });
+
+ consumer.run({
+ eachMessage: async ({ topic, partition, message }) => {
+ console.log({
+ topic,
+ partition,
+ offset: message.offset,
+ key: message.key?.toString(),
+ value: message.value.toString(),
+ });
+ }
+ });
+
+ // Update stopped whenever we're done consuming.
+ // The update can be in another async function or scheduled with setTimeout etc.
+ while(!stopped) {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+
+ await consumer.disconnect();
+}
+
+consumerStart();
+```
+
+See the examples in the [examples](examples) directory for more in-depth examples.
\ No newline at end of file
diff --git a/README.md b/README.md
index cdba3ce0..f07ea273 100644
--- a/README.md
+++ b/README.md
@@ -1,631 +1,106 @@
-confluent-kafka-js - Node.js wrapper for Kafka C/C++ library
-==============================================
+Confluent's JavaScript Client for Apache KafkaTM
+=====================================================
-Copyright (c) 2016-2023 Blizzard Entertainment.
+**confluent-kafka-javascript** is Confluent's JavaScript client for [Apache Kafka](http://kafka.apache.org/) and the
+[Confluent Platform](https://www.confluent.io/product/compare/). This is an **limited availability** library. The goal is to provide an highly performant, reliable and easy to use JavaScript client that is based on [node-rdkafka](https://github.com/Blizzard/node-rdkafka) yet also API compatible with [KafkaJS](https://github.com/tulios/kafkajs) to provide flexibility to users and streamline migrations from other clients.
-[https://github.com/confluentinc/confluent-kafka-js](https://github.com/confluentinc/confluent-kafka-js)
+Features:
-# Looking for Collaborators!
+- **High performance** - confluent-kafka-javascript is a lightweight wrapper around
+[librdkafka](https://github.com/confluentinc/librdkafka), a finely tuned C
+client.
-I am looking for *your* help to make this project even better! If you're interested, check [this out](https://github.com/confluentinc/confluent-kafka-js/issues/628)
+- **Reliability** - There are a lot of details to get right when writing an Apache Kafka
+client. We get them right in one place (librdkafka) and leverage this work
+across all of our clients.
-# Overview
+- **Supported** - Commercial support is offered by [Confluent](https://confluent.io/).
-The `confluent-kafka-js` library is a high-performance NodeJS client for [Apache Kafka](http://kafka.apache.org/) that wraps the native [librdkafka](https://github.com/edenhill/librdkafka) library. All the complexity of balancing writes across partitions and managing (possibly ever-changing) brokers should be encapsulated in the library.
+- **Future proof** - Confluent, founded by the
+creators of Kafka, is building a [streaming platform](https://www.confluent.io/product/)
+with Apache Kafka at its core. It's high priority for us that client features keep
+pace with core Apache Kafka and components of the [Confluent Platform](https://www.confluent.io/product/).
-__This library currently uses `librdkafka` version `2.3.0`.__
+This library leverages the work and concepts from two popular Apache Kafka JavaScript clients: [node-rdkafka](https://github.com/Blizzard/node-rdkafka) and [KafkaJS](https://github.com/tulios/kafkajs). The core is heavily based on the node-rdkafka library, which uses our own [librdkafka](https://github.com/confluentinc/librdkafka) library for core client functionality. However, we leverage a promisified API and a more idiomatic interface, similar to the one in KafkaJS, making it easy for developers to migrate and adopt this client depending on the patterns and interface they prefer. We're very happy to have been able to leverage the excellent work of the many authors of these libraries!
-## Reference Docs
+### This library is currently in limited-availability
-To view the reference docs for the current version, go [here](https://confluentinc.github.io/confluent-kafka-js/current/)
+To use **Schema Registry**, use the existing [@confluentinc/schemaregistry](https://www.npmjs.com/package/@confluentinc/schemaregistry) library that is compatible with this library. For a simple schema registry example, see [sr.js](https://github.com/confluentinc/confluent-kafka-javascript/blob/dev_early_access_development_branch/examples/kafkajs/sr.js).
-## Contributing
-
-For guidelines on contributing please see [CONTRIBUTING.md](https://github.com/confluentinc/confluent-kafka-js/blob/master/CONTRIBUTING.md)
-
-## Code of Conduct
-
-Play nice; Play fair.
## Requirements
-* Apache Kafka >=0.9
-* Node.js >=4
-* Linux/Mac
-* Windows?! See below
-* OpenSSL
-
-### Mac OS High Sierra / Mojave
-
-OpenSSL has been upgraded in High Sierra and homebrew does not overwrite default system libraries. That means when building confluent-kafka-js, because you are using openssl, you need to tell the linker where to find it:
-
-```sh
-export CPPFLAGS=-I/usr/local/opt/openssl/include
-export LDFLAGS=-L/usr/local/opt/openssl/lib
-```
-
-Then you can run `npm install` on your application to get it to build correctly.
-
-__NOTE:__ From the `librdkafka` docs
-
-> WARNING: Due to a bug in Apache Kafka 0.9.0.x, the ApiVersionRequest (as sent by the client when connecting to the broker) will be silently ignored by the broker causing the request to time out after 10 seconds. This causes client-broker connections to stall for 10 seconds during connection-setup before librdkafka falls back on the `broker.version.fallback` protocol features. The workaround is to explicitly configure `api.version.request` to `false` on clients communicating with <=0.9.0.x brokers.
-
-### Alpine
-
-Using Alpine Linux? Check out the [docs](https://github.com/confluentinc/confluent-kafka-js/blob/master/examples/docker-alpine.md).
-
-### Windows
-
-Windows build **is not** compiled from `librdkafka` source but it is rather linked against the appropriate version of [NuGet librdkafka.redist](https://www.nuget.org/packages/librdkafka.redist/) static binary that gets downloaded from `https://globalcdn.nuget.org/packages/librdkafka.redist.2.3.0.nupkg` during installation. This download link can be changed using the environment variable `NODE_RDKAFKA_NUGET_BASE_URL` that defaults to `https://globalcdn.nuget.org/packages/` when it's no set.
-
-Requirements:
- * [node-gyp for Windows](https://github.com/nodejs/node-gyp#on-windows)
-
-**Note:** I _still_ do not recommend using `confluent-kafka-js` in production on Windows. This feature was in high demand and is provided to help develop, but we do not test against Windows, and windows support may lag behind Linux/Mac support because those platforms are the ones used to develop this library. Contributors are welcome if any Windows issues are found :)
-
-## Tests
-
-This project includes two types of unit tests in this project:
-* end-to-end integration tests
-* unit tests
-
-You can run both types of tests by using `Makefile`. Doing so calls `mocha` in your locally installed `node_modules` directory.
-
-* Before you run the tests, be sure to init and update the submodules:
- 1. `git submodule init`
- 2. `git submodule update`
-* To run the unit tests, you can run `make lint` or `make test`.
-* To run the integration tests, you must have a running Kafka installation available. By default, the test tries to connect to `localhost:9092`; however, you can supply the `KAFKA_HOST` environment variable to override this default behavior. Run `make e2e`.
-
-# Usage
-
-You can install the `confluent-kafka-js` module like any other module:
-
-```
-npm install confluent-kafka-js
-```
-
-To use the module, you must `require` it.
-
-```js
-const Kafka = require('confluent-kafka-js');
-```
-
-## Configuration
-
-You can pass many configuration options to `librdkafka`. A full list can be found in `librdkafka`'s [Configuration.md](https://github.com/edenhill/librdkafka/blob/v2.3.0/CONFIGURATION.md)
-
-Configuration keys that have the suffix `_cb` are designated as callbacks. Some
-of these keys are informational and you can choose to opt-in (for example, `dr_cb`). Others are callbacks designed to
-return a value, such as `partitioner_cb`.
-
-Not all of these options are supported.
-The library will throw an error if the value you send in is invalid.
-
-The library currently supports the following callbacks:
-* `partitioner_cb`
-* `dr_cb` or `dr_msg_cb`
-* `event_cb`
-* `rebalance_cb` (see [Rebalancing](#rebalancing))
-* `offset_commit_cb` (see [Commits](#commits))
-
-### Librdkafka Methods
-
-This library includes two utility functions for detecting the status of your installation. Please try to include these when making issue reports where applicable.
-
-You can get the features supported by your compile of `librdkafka` by reading the variable "features" on the root of the `confluent-kafka-js` object.
-
-```js
-const Kafka = require('confluent-kafka-js');
-console.log(Kafka.features);
-
-// #=> [ 'gzip', 'snappy', 'ssl', 'sasl', 'regex', 'lz4' ]
-```
-
-You can also get the version of `librdkafka`
-
-```js
-const Kafka = require('confluent-kafka-js');
-console.log(Kafka.librdkafkaVersion);
-
-// #=> 2.3.0
-```
-
-## Sending Messages
-
-A `Producer` sends messages to Kafka. The `Producer` constructor takes a configuration object, as shown in the following example:
-
-```js
-const producer = new Kafka.Producer({
- 'metadata.broker.list': 'kafka-host1:9092,kafka-host2:9092'
-});
-```
-
-A `Producer` requires only `metadata.broker.list` (the Kafka brokers) to be created. The values in this list are separated by commas. For other configuration options, see the [Configuration.md](https://github.com/edenhill/librdkafka/blob/v2.3.0/CONFIGURATION.md) file described previously.
-
-The following example illustrates a list with several `librdkafka` options set.
-
-```js
-const producer = new Kafka.Producer({
- 'client.id': 'kafka',
- 'metadata.broker.list': 'localhost:9092',
- 'compression.codec': 'gzip',
- 'retry.backoff.ms': 200,
- 'message.send.max.retries': 10,
- 'socket.keepalive.enable': true,
- 'queue.buffering.max.messages': 100000,
- 'queue.buffering.max.ms': 1000,
- 'batch.num.messages': 1000000,
- 'dr_cb': true
-});
-```
-
-#### Stream API
-
-You can easily use the `Producer` as a writable stream immediately after creation (as shown in the following example):
-
-```js
-// Our producer with its Kafka brokers
-// This call returns a new writable stream to our topic 'topic-name'
-const stream = Kafka.Producer.createWriteStream({
- 'metadata.broker.list': 'kafka-host1:9092,kafka-host2:9092'
-}, {}, {
- topic: 'topic-name'
-});
-
-// Writes a message to the stream
-const queuedSuccess = stream.write(Buffer.from('Awesome message'));
-
-if (queuedSuccess) {
- console.log('We queued our message!');
-} else {
- // Note that this only tells us if the stream's queue is full,
- // it does NOT tell us if the message got to Kafka! See below...
- console.log('Too many messages in our queue already');
-}
-
-// NOTE: MAKE SURE TO LISTEN TO THIS IF YOU WANT THE STREAM TO BE DURABLE
-// Otherwise, any error will bubble up as an uncaught exception.
-stream.on('error', (err) => {
- // Here's where we'll know if something went wrong sending to Kafka
- console.error('Error in our kafka stream');
- console.error(err);
-})
-```
-
-If you do not want your code to crash when an error happens, ensure you have an `error` listener on the stream. Most errors are not necessarily fatal, but the ones that are will immediately destroy the stream. If you use `autoClose`, the stream will close itself at the first sign of a problem.
-
-#### Standard API
-
-The Standard API is more performant, particularly when handling high volumes of messages.
-However, it requires more manual setup to use. The following example illustrates its use:
-
-```js
-const producer = new Kafka.Producer({
- 'metadata.broker.list': 'localhost:9092',
- 'dr_cb': true
-});
-
-// Connect to the broker manually
-producer.connect();
-
-// Wait for the ready event before proceeding
-producer.on('ready', () => {
- try {
- producer.produce(
- // Topic to send the message to
- 'topic',
- // optionally we can manually specify a partition for the message
- // this defaults to -1 - which will use librdkafka's default partitioner (consistent random for keyed messages, random for unkeyed messages)
- null,
- // Message to send. Must be a buffer
- Buffer.from('Awesome message'),
- // for keyed messages, we also specify the key - note that this field is optional
- 'Stormwind',
- // you can send a timestamp here. If your broker version supports it,
- // it will get added. Otherwise, we default to 0
- Date.now(),
- // you can send an opaque token here, which gets passed along
- // to your delivery reports
- );
- } catch (err) {
- console.error('A problem occurred when sending our message');
- console.error(err);
- }
-});
-
-// Any errors we encounter, including connection errors
-producer.on('event.error', (err) => {
- console.error('Error from producer');
- console.error(err);
-})
-
-// We must either call .poll() manually after sending messages
-// or set the producer to poll on an interval (.setPollInterval).
-// Without this, we do not get delivery events and the queue
-// will eventually fill up.
-producer.setPollInterval(100);
-```
-
-To see the configuration options available to you, see the [Configuration](#configuration) section.
-
-##### Methods
-
-|Method|Description|
-|-------|----------|
-|`producer.connect()`| Connects to the broker.
The `connect()` method emits the `ready` event when it connects successfully. If it does not, the error will be passed through the callback. |
-|`producer.disconnect()`| Disconnects from the broker.
The `disconnect()` method emits the `disconnected` event when it has disconnected. If it does not, the error will be passed through the callback. |
-|`producer.poll()` | Polls the producer for delivery reports or other events to be transmitted via the emitter.
In order to get the events in `librdkafka`'s queue to emit, you must call this regularly. |
-|`producer.setPollInterval(interval)` | Polls the producer on this interval, handling disconnections and reconnection. Set it to 0 to turn it off. |
-|`producer.produce(topic, partition, msg, key, timestamp, opaque)`| Sends a message.
The `produce()` method throws when produce would return an error. Ordinarily, this is just if the queue is full. |
-|`producer.flush(timeout, callback)`| Flush the librdkafka internal queue, sending all messages. Default timeout is 500ms |
-|`producer.initTransactions(timeout, callback)`| Initializes the transactional producer. |
-|`producer.beginTransaction(callback)`| Starts a new transaction. |
-|`producer.sendOffsetsToTransaction(offsets, consumer, timeout, callback)`| Sends consumed topic-partition-offsets to the broker, which will get committed along with the transaction. |
-|`producer.abortTransaction(timeout, callback)`| Aborts the ongoing transaction. |
-|`producer.commitTransaction(timeout, callback)`| Commits the ongoing transaction. |
-
-##### Events
-
-Some configuration properties that end in `_cb` indicate that an event should be generated for that option. You can either:
-
-* provide a value of `true` and react to the event
-* provide a callback function directly
+The following configurations are supported:
-The following example illustrates an event:
+* Any supported version of Node.js (The two LTS versions, 18 and 20, and the latest versions, 21 and 22).
+* Linux (x64 and arm64) - both glibc and musl/alpine.
+* macOS - arm64/m1.
+* Windows - x64.
-```js
-const producer = new Kafka.Producer({
- 'client.id': 'my-client', // Specifies an identifier to use to help trace activity in Kafka
- 'metadata.broker.list': 'localhost:9092', // Connect to a Kafka instance on localhost
- 'dr_cb': true // Specifies that we want a delivery-report event to be generated
-});
+Installation on any of these platforms is meant to be seamless, without any C/C++ compilation required.
-// Poll for events every 100 ms
-producer.setPollInterval(100);
+In case your system configuration is not within the supported ones, [a supported version of Python](https://devguide.python.org/versions/) must be available on the system for the installation process. [This is required for the `node-gyp` build tool.](https://github.com/nodejs/node-gyp?tab=readme-ov-file#configuring-python-dependency).
-producer.on('delivery-report', (err, report) => {
- // Report of delivery statistics here:
- //
- console.log(report);
-});
+```bash
+npm install @confluentinc/kafka-javascript
```
-The following table describes types of events.
+Yarn and pnpm support is experimental.
-|Event|Description|
-|-------|----------|
-| `disconnected` | The `disconnected` event is emitted when the broker has disconnected.
This event is emitted only when `.disconnect` is called. The wrapper will always try to reconnect otherwise. |
-| `ready` | The `ready` event is emitted when the `Producer` is ready to send messages. |
-| `event` | The `event` event is emitted when `librdkafka` reports an event (if you opted in via the `event_cb` option). |
-| `event.log` | The `event.log` event is emitted when logging events come in (if you opted into logging via the `event_cb` option).
You will need to set a value for `debug` if you want to send information. |
-| `event.stats` | The `event.stats` event is emitted when `librdkafka` reports stats (if you opted in by setting the `statistics.interval.ms` to a non-zero value). |
-| `event.error` | The `event.error` event is emitted when `librdkafka` reports an error |
-| `event.throttle` | The `event.throttle` event emitted when `librdkafka` reports throttling. |
-| `delivery-report` | The `delivery-report` event is emitted when a delivery report has been found via polling.
To use this event, you must set `request.required.acks` to `1` or `-1` in topic configuration and `dr_cb` (or `dr_msg_cb` if you want the report to contain the message payload) to `true` in the `Producer` constructor options. |
+# Getting Started
-### Higher Level Producer
+Below is a simple produce example for users migrating from KafkaJS.
-The higher level producer is a variant of the producer which can propagate callbacks to you upon message delivery.
+```javascript
+// require('kafkajs') is replaced with require('@confluentinc/kafka-javascript').KafkaJS.
+const { Kafka } = require("@confluentinc/kafka-javascript").KafkaJS;
-```js
-const producer = new Kafka.HighLevelProducer({
- 'metadata.broker.list': 'localhost:9092',
-});
-```
-
-This will enrich the produce call so it will have a callback to tell you when the message has been delivered. You lose the ability to specify opaque tokens.
-
-```js
-producer.produce(topicName, null, Buffer.from('alliance4ever'), null, Date.now(), (err, offset) => {
- // The offset if our acknowledgement level allows us to receive delivery offsets
- console.log(offset);
-});
-```
-
-Additionally you can add serializers to modify the value of a produce for a key or value before it is sent over to Kafka.
-
-```js
-producer.setValueSerializer((value) => {
- return Buffer.from(JSON.stringify(value));
-});
-```
-
-Otherwise the behavior of the class should be exactly the same.
-
-## Kafka.KafkaConsumer
-
-To read messages from Kafka, you use a `KafkaConsumer`. You instantiate a `KafkaConsumer` object as follows:
-
-```js
-const consumer = new Kafka.KafkaConsumer({
- 'group.id': 'kafka',
- 'metadata.broker.list': 'localhost:9092',
-}, {});
-```
-
-The first parameter is the global config, while the second parameter is the topic config that gets applied to all subscribed topics. To view a list of all supported configuration properties, see the [Configuration.md](https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md) file described previously. Look for the `C` and `*` keys.
-
-The `group.id` and `metadata.broker.list` properties are required for a consumer.
-
-### Rebalancing
-
-Rebalancing is managed internally by `librdkafka` by default. If you would like to override this functionality, you may provide your own logic as a rebalance callback.
-
-```js
-const consumer = new Kafka.KafkaConsumer({
- 'group.id': 'kafka',
- 'metadata.broker.list': 'localhost:9092',
- 'rebalance_cb': (err, assignment) => {
-
- if (err.code === Kafka.CODES.ERRORS.ERR__ASSIGN_PARTITIONS) {
- // Note: this can throw when you are disconnected. Take care and wrap it in
- // a try catch if that matters to you
- this.assign(assignment);
- } else if (err.code == Kafka.CODES.ERRORS.ERR__REVOKE_PARTITIONS){
- // Same as above
- this.unassign();
- } else {
- // We had a real error
- console.error(err);
- }
-
- }
-})
-```
-
-`this` is bound to the `KafkaConsumer` you have created. By specifying a `rebalance_cb` you can also listen to the `rebalance` event as an emitted event. This event is not emitted when using the internal `librdkafka` rebalancer.
+async function producerStart() {
+ const kafka = new Kafka({
+ kafkaJS: {
+ brokers: [''],
+ ssl: true,
+ sasl: {
+ mechanism: 'plain',
+ username: '',
+ password: '',
+ },
+ }
+ });
-### Commits
+ const producer = kafka.producer();
-When you commit in `confluent-kafka-js`, the standard way is to queue the commit request up with the next `librdkafka` request to the broker. When doing this, there isn't a way to know the result of the commit. Luckily there is another callback you can listen to to get this information
+ await producer.connect();
-```js
-const consumer = new Kafka.KafkaConsumer({
- 'group.id': 'kafka',
- 'metadata.broker.list': 'localhost:9092',
- 'offset_commit_cb': (err, topicPartitions) => {
+ console.log("Connected successfully");
- if (err) {
- // There was an error committing
- console.error(err);
- } else {
- // Commit went through. Let's log the topic partitions
- console.log(topicPartitions);
+ const res = []
+ for (let i = 0; i < 50; i++) {
+ res.push(producer.send({
+ topic: 'test-topic',
+ messages: [
+ { value: 'v222', partition: 0 },
+ { value: 'v11', partition: 0, key: 'x' },
+ ]
+ }));
}
+ await Promise.all(res);
- }
-})
-```
-
-`this` is bound to the `KafkaConsumer` you have created. By specifying an `offset_commit_cb` you can also listen to the `offset.commit` event as an emitted event. It receives an error and the list of topic partitions as argument. This is not emitted unless opted in.
-
-### Message Structure
-
-Messages that are returned by the `KafkaConsumer` have the following structure.
-
-```js
-{
- value: Buffer.from('hi'), // message contents as a Buffer
- size: 2, // size of the message, in bytes
- topic: 'librdtesting-01', // topic the message comes from
- offset: 1337, // offset the message was read from
- partition: 1, // partition the message was on
- key: 'someKey', // key of the message if present
- timestamp: 1510325354780 // timestamp of message creation
-}
-```
-
-### Stream API
-
-The stream API is the easiest way to consume messages. The following example illustrates the use of the stream API:
-
-```js
-// Read from the librdtesting-01 topic... note that this creates a new stream on each call!
-const stream = KafkaConsumer.createReadStream(globalConfig, topicConfig, {
- topics: ['librdtesting-01']
-});
+ await producer.disconnect();
-stream.on('data', (message) => {
- console.log('Got message');
- console.log(message.value.toString());
-});
-```
-
-You can also get the `consumer` from the streamConsumer, for using consumer methods. The following example illustrates that:
-
-```js
-stream.consumer.commit(); // Commits all locally stored offsets
-```
-
-### Standard API
-
-You can also use the Standard API and manage callbacks and events yourself. You can choose different modes for consuming messages:
-
-* *Flowing mode*. This mode flows all of the messages it can read by maintaining an infinite loop in the event loop. It only stops when it detects the consumer has issued the `unsubscribe` or `disconnect` method.
-* *Non-flowing mode*. This mode reads a single message from Kafka at a time manually.
-
-The following example illustrates flowing mode:
-```js
-// Flowing mode
-consumer.connect();
-
-consumer
- .on('ready', () => {
- consumer.subscribe(['librdtesting-01']);
-
- // Consume from the librdtesting-01 topic. This is what determines
- // the mode we are running in. By not specifying a callback (or specifying
- // only a callback) we get messages as soon as they are available.
- consumer.consume();
- })
- .on('data', (data) => {
- // Output the actual message contents
- console.log(data.value.toString());
- });
-```
-The following example illustrates non-flowing mode:
-```js
-// Non-flowing mode
-consumer.connect();
-
-consumer
- .on('ready', () => {
- // Subscribe to the librdtesting-01 topic
- // This makes subsequent consumes read from that topic.
- consumer.subscribe(['librdtesting-01']);
-
- // Read one message every 1000 milliseconds
- setInterval(() => {
- consumer.consume(1);
- }, 1000);
- })
- .on('data', (data) => {
- console.log('Message found! Contents below.');
- console.log(data.value.toString());
- });
-```
-
-The following table lists important methods for this API.
-
-|Method|Description|
-|-------|----------|
-|`consumer.connect()` | Connects to the broker.
The `connect()` emits the event `ready` when it has successfully connected. If it does not, the error will be passed through the callback. |
-|`consumer.disconnect()` | Disconnects from the broker.
The `disconnect()` method emits `disconnected` when it has disconnected. If it does not, the error will be passed through the callback. |
-|`consumer.subscribe(topics)` | Subscribes to an array of topics. |
-|`consumer.unsubscribe()` | Unsubscribes from the currently subscribed topics.
You cannot subscribe to different topics without calling the `unsubscribe()` method first. |
-|`consumer.consume(cb)` | Gets messages from the existing subscription as quickly as possible. If `cb` is specified, invokes `cb(err, message)`.
This method keeps a background thread running to do the work. Note that the number of threads in nodejs process is limited by `UV_THREADPOOL_SIZE` (default value is 4) and using up all of them blocks other parts of the application that need threads. If you need multiple consumers then consider increasing `UV_THREADPOOL_SIZE` or using `consumer.consume(number, cb)` instead. |
-|`consumer.consume(number, cb)` | Gets `number` of messages from the existing subscription. If `cb` is specified, invokes `cb(err, message)`. |
-|`consumer.commit()` | Commits all locally stored offsets |
-|`consumer.commit(topicPartition)` | Commits offsets specified by the topic partition |
-|`consumer.commitMessage(message)` | Commits the offsets specified by the message |
-
-The following table lists events for this API.
-
-|Event|Description|
-|-------|----------|
-|`data` | When using the Standard API consumed messages are emitted in this event. |
-|`partition.eof` | When using Standard API and the configuration option `enable.partition.eof` is set, `partition.eof` events are emitted in this event. The event contains `topic`, `partition` and `offset` properties. |
-|`warning` | The event is emitted in case of `UNKNOWN_TOPIC_OR_PART` or `TOPIC_AUTHORIZATION_FAILED` errors when consuming in *Flowing mode*. Since the consumer will continue working if the error is still happening, the warning event should reappear after the next metadata refresh. To control the metadata refresh rate set `topic.metadata.refresh.interval.ms` property. Once you resolve the error, you can manually call `getMetadata` to speed up consumer recovery. |
-|`disconnected` | The `disconnected` event is emitted when the broker disconnects.
This event is only emitted when `.disconnect` is called. The wrapper will always try to reconnect otherwise. |
-|`ready` | The `ready` event is emitted when the `Consumer` is ready to read messages. |
-|`event` | The `event` event is emitted when `librdkafka` reports an event (if you opted in via the `event_cb` option).|
-|`event.log` | The `event.log` event is emitted when logging events occur (if you opted in for logging via the `event_cb` option).
You will need to set a value for `debug` if you want information to send. |
-|`event.stats` | The `event.stats` event is emitted when `librdkafka` reports stats (if you opted in by setting the `statistics.interval.ms` to a non-zero value). |
-|`event.error` | The `event.error` event is emitted when `librdkafka` reports an error |
-|`event.throttle` | The `event.throttle` event is emitted when `librdkafka` reports throttling.|
-
-## Reading current offsets from the broker for a topic
-
-Some times you find yourself in the situation where you need to know the latest (and earliest) offset for one of your topics. Connected producers and consumers both allow you to query for these through `queryWaterMarkOffsets` like follows:
-
-```js
-const timeout = 5000, partition = 0;
-consumer.queryWatermarkOffsets('my-topic', partition, timeout, (err, offsets) => {
- const high = offsets.highOffset;
- const low = offsets.lowOffset;
-});
-
-producer.queryWatermarkOffsets('my-topic', partition, timeout, (err, offsets) => {
- const high = offsets.highOffset;
- const low = offsets.lowOffset;
-});
-
-An error will be returned if the client was not connected or the request timed out within the specified interval.
-
-```
-
-## Metadata
-
-Both `Kafka.Producer` and `Kafka.KafkaConsumer` include a `getMetadata` method to retrieve metadata from Kafka.
-
-Getting metadata on any connection returns the following data structure:
-
-```js
-{
- orig_broker_id: 1,
- orig_broker_name: "broker_name",
- brokers: [
- {
- id: 1,
- host: 'localhost',
- port: 40
- }
- ],
- topics: [
- {
- name: 'awesome-topic',
- partitions: [
- {
- id: 1,
- leader: 20,
- replicas: [1, 2],
- isrs: [1, 2]
- }
- ]
- }
- ]
+ console.log("Disconnected successfully");
}
-```
-
-The following example illustrates how to use the `getMetadata` method.
-
-When fetching metadata for a specific topic, if a topic reference does not exist, one is created using the default config.
-Please see the documentation on `Client.getMetadata` if you want to set configuration parameters, e.g. `acks`, on a topic to produce messages to.
-
-```js
-const opts = {
- topic: 'librdtesting-01',
- timeout: 10000
-};
-
-producer.getMetadata(opts, (err, metadata) => {
- if (err) {
- console.error('Error getting metadata');
- console.error(err);
- } else {
- console.log('Got metadata');
- console.log(metadata);
- }
-});
-```
-## Admin Client
-
-`confluent-kafka-js` now supports the admin client for creating, deleting, and scaling out topics. The `librdkafka` APIs also support altering configuration of topics and broker, but that is not currently implemented.
-
-To create an Admin client, you can do as follows:
-
-```js
-const Kafka = require('confluent-kafka-js');
-
-const client = Kafka.AdminClient.create({
- 'client.id': 'kafka-admin',
- 'metadata.broker.list': 'broker01'
-});
+producerStart();
```
-This will instantiate the `AdminClient`, which will allow the calling of the admin methods.
+1. If you're migrating from `kafkajs`, you can use the [migration guide](MIGRATION.md#kafkajs).
+2. If you're migrating from `node-rdkafka`, you can use the [migration guide](MIGRATION.md#node-rdkafka).
+3. If you're starting afresh, you can use the [quickstart guide](QUICKSTART.md).
-```js
-client.createTopic({
- topic: topicName,
- num_partitions: 1,
- replication_factor: 1
-}, (err) => {
- // Done!
-});
-```
-
-All of the admin api methods can have an optional timeout as their penultimate parameter.
+An in-depth reference may be found at [INTRODUCTION.md](INTRODUCTION.md).
-The following table lists important methods for this API.
-
-|Method|Description|
-|-------|----------|
-|`client.disconnect()` | Destroy the admin client, making it invalid for further use. |
-|`client.createTopic(topic, timeout, cb)` | Create a topic on the broker with the given configuration. See JS doc for more on structure of the topic object |
-|`client.deleteTopic(topicName, timeout, cb)` | Delete a topic of the given name |
-|`client.createPartitions(topicName, desiredPartitions, timeout, cb)` | Create partitions until the topic has the desired number of partitions. |
+## Contributing
-Check the tests for an example of how to use this API!
+Bug reports and feedback is appreciated in the form of Github Issues.
+For guidelines on contributing please see [CONTRIBUTING.md](CONTRIBUTING.md)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..64bc0e3d
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Supported Versions
+
+Post version 1.0.0, the latest version will be supported.
+
+## Reporting a Vulnerability
+
+To report a vulnerability, please notify security@confluent.io
+
+If an issue is confirmed, a github issue will be created to help track progress with its resolution.
diff --git a/bench/consumer-raw-rdkafka.js b/bench/consumer-raw-rdkafka.js
index 5b27c1d2..a42461e8 100644
--- a/bench/consumer-raw-rdkafka.js
+++ b/bench/consumer-raw-rdkafka.js
@@ -1,5 +1,5 @@
/*
- * confluent-kafka-js - Node.js wrapper for RdKafka C/C++ library
+ * confluent-kafka-javascript - Node.js wrapper for RdKafka C/C++ library
*
* Copyright (c) 2016-2023 Blizzard Entertainment
*
@@ -16,7 +16,7 @@ var topic = process.argv[3] || 'test';
var consumer = new Kafka.KafkaConsumer({
'metadata.broker.list': host,
- 'group.id': 'confluent-kafka-js-bench-s',
+ 'group.id': 'confluent-kafka-javascript-bench-s',
'fetch.wait.max.ms': 100,
'fetch.message.max.bytes': 1024 * 1024,
'enable.auto.commit': false
diff --git a/bench/consumer-subscribe.js b/bench/consumer-subscribe.js
index 6dbd7fc4..f46ee21d 100644
--- a/bench/consumer-subscribe.js
+++ b/bench/consumer-subscribe.js
@@ -1,5 +1,5 @@
/*
- * confluent-kafka-js - Node.js wrapper for RdKafka C/C++ library
+ * confluent-kafka-javascript - Node.js wrapper for RdKafka C/C++ library
*
* Copyright (c) 2016-2023 Blizzard Entertainment
*
@@ -16,7 +16,7 @@ var topic = process.argv[3] || 'test';
var consumer = new Kafka.KafkaConsumer({
'metadata.broker.list': host,
- 'group.id': 'confluent-kafka-js-bench',
+ 'group.id': 'confluent-kafka-javascript-bench',
'fetch.wait.max.ms': 100,
'fetch.message.max.bytes': 1024 * 1024,
'enable.auto.commit': false
diff --git a/bench/kafka-consumer-stream.js b/bench/kafka-consumer-stream.js
index ff8888f9..6bf53779 100644
--- a/bench/kafka-consumer-stream.js
+++ b/bench/kafka-consumer-stream.js
@@ -1,5 +1,5 @@
/*
- * confluent-kafka-js - Node.js wrapper for RdKafka C/C++ library
+ * confluent-kafka-javascript - Node.js wrapper for RdKafka C/C++ library
*
* Copyright (c) 2016-2023 Blizzard Entertainment
*
@@ -18,7 +18,7 @@ var topic = process.argv[3] || 'test';
var stream = Kafka.createReadStream({
'metadata.broker.list': host,
- 'group.id': 'confluent-kafka-js-benchs',
+ 'group.id': 'confluent-kafka-javascript-benchs',
'fetch.wait.max.ms': 100,
'fetch.message.max.bytes': 1024 * 1024,
'enable.auto.commit': false
diff --git a/bench/producer-raw-rdkafka.js b/bench/producer-raw-rdkafka.js
index e5d7cf56..c0d55e5c 100644
--- a/bench/producer-raw-rdkafka.js
+++ b/bench/producer-raw-rdkafka.js
@@ -1,5 +1,5 @@
/*
- * confluent-kafka-js - Node.js wrapper for RdKafka C/C++ library
+ * confluent-kafka-javascript - Node.js wrapper for RdKafka C/C++ library
*
* Copyright (c) 2016-2023 Blizzard Entertainment
*
@@ -24,7 +24,7 @@ var MAX = process.argv[5] || 10000000;
var producer = new Kafka.Producer({
'metadata.broker.list': host,
- 'group.id': 'confluent-kafka-js-bench',
+ 'group.id': 'confluent-kafka-javascript-bench',
'compression.codec': compression,
'retry.backoff.ms': 200,
'message.send.max.retries': 10,
diff --git a/bench/producer-rdkafka.js b/bench/producer-rdkafka.js
index d2fa37b2..97eb6bf6 100644
--- a/bench/producer-rdkafka.js
+++ b/bench/producer-rdkafka.js
@@ -1,5 +1,5 @@
/*
- * confluent-kafka-js - Node.js wrapper for RdKafka C/C++ library
+ * confluent-kafka-javascript - Node.js wrapper for RdKafka C/C++ library
*
* Copyright (c) 2016-2023 Blizzard Entertainment
*
@@ -20,7 +20,7 @@ var MAX = process.argv[5] || 1000000;
var stream = Kafka.Producer.createWriteStream({
'metadata.broker.list': host,
- 'group.id': 'confluent-kafka-js-bench',
+ 'group.id': 'confluent-kafka-javascript-bench',
'compression.codec': compression,
'retry.backoff.ms': 200,
'message.send.max.retries': 10,
diff --git a/binding.gyp b/binding.gyp
index 9ec16668..a70df362 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -7,7 +7,7 @@
},
"targets": [
{
- "target_name": "confluent-kafka-js",
+ "target_name": "confluent-kafka-javascript",
'sources': [
'src/binding.cc',
'src/callbacks.cc',
@@ -86,34 +86,36 @@
],
'conditions': [
[
- ['OS=="linux"', 'CKJS_LINKING="dynamic"'],
+ 'CKJS_LINKING=="dynamic"',
{
- "libraries": [
- "../build/deps/librdkafka.so",
- "../build/deps/librdkafka++.so",
- "-Wl,-rpath='$$ORIGIN/../deps'",
- ],
- }
- ],
- [
- ['OS=="linux"', 'CKJS_LINKING!="dynamic"'],
+ "conditions": [
+ [
+ 'OS=="mac"',
+ {
+ "libraries": [
+ "../build/deps/librdkafka.dylib",
+ "../build/deps/librdkafka++.dylib",
+ "-Wl,-rpath,'$$ORIGIN/../deps'",
+ ],
+ },
+ {
+ "libraries": [
+ "../build/deps/librdkafka.so",
+ "../build/deps/librdkafka++.so",
+ "-Wl,-rpath,'$$ORIGIN/../deps'",
+ ],
+ },
+ ]
+ ]
+ },
{
"libraries": [
"../build/deps/librdkafka-static.a",
"../build/deps/librdkafka++.a",
- "-Wl,-rpath='$$ORIGIN/../deps'",
+ "-Wl,-rpath,'$$ORIGIN/../deps'",
],
}
],
- [
- 'OS=="mac"',
- {
- "libraries": [
- "../build/deps/librdkafka.dylib",
- "../build/deps/librdkafka++.dylib",
- ],
- }
- ]
],
},
# Else link against globally installed rdkafka and use
diff --git a/buf.gen.yaml b/buf.gen.yaml
new file mode 100644
index 00000000..7fe5b593
--- /dev/null
+++ b/buf.gen.yaml
@@ -0,0 +1,8 @@
+# Learn more: https://buf.build/docs/configuration/v2/buf-gen-yaml
+version: v2
+inputs:
+ - directory: proto
+plugins:
+ - local: protoc-gen-es
+ opt: target=ts
+ out: schemaregistry
diff --git a/ci/checks/librdkafka-correct-version.js b/ci/checks/librdkafka-correct-version.js
index 5b2506e5..011a5481 100644
--- a/ci/checks/librdkafka-correct-version.js
+++ b/ci/checks/librdkafka-correct-version.js
@@ -61,7 +61,8 @@ function versionAsString(version) {
const librdkafkaVersion = parseLibrdkafkaVersion(defines.RD_KAFKA_VERSION);
const versionString = versionAsString(librdkafkaVersion);
-if (pjs.librdkafka !== versionString) {
+// If our version is a devel (early access) version, we might be on master.
+if (pjs.librdkafka !== versionString && !pjs.version.includes('devel')) {
console.error(`Librdkafka version of ${versionString} does not match package json: ${pjs.librdkafka}`);
process.exit(1);
}
diff --git a/ci/librdkafka-defs-generator.js b/ci/librdkafka-defs-generator.js
index be4113a1..f81b423e 100644
--- a/ci/librdkafka-defs-generator.js
+++ b/ci/librdkafka-defs-generator.js
@@ -52,7 +52,7 @@ function extractConfigItems(configStr) {
}
function processItem(configItem) {
- // These items are overwritten by confluent-kafka-js
+ // These items are overwritten by confluent-kafka-javascript
switch (configItem.property) {
case 'dr_msg_cb':
return { ...configItem, type: 'boolean' };
diff --git a/ci/tests/run_perf_test.sh b/ci/tests/run_perf_test.sh
new file mode 100755
index 00000000..c55a67ab
--- /dev/null
+++ b/ci/tests/run_perf_test.sh
@@ -0,0 +1,64 @@
+#!/bin/bash
+
+testresultConfluentProducerConsumer=$(mktemp)
+testresultConfluentCtp=$(mktemp)
+testresultKjsProducerConsumer=$(mktemp)
+testresultKjsCtp=$(mktemp)
+
+MODE=confluent MESSAGE_COUNT=500000 node performance-consolidated.js --create-topics --consumer --producer 2>&1 | tee "$testresultConfluentProducerConsumer"
+MODE=kafkajs MESSAGE_COUNT=500000 node performance-consolidated.js --create-topics --consumer --producer 2>&1 | tee "$testresultKjsProducerConsumer"
+MODE=confluent MESSAGE_COUNT=5000 node performance-consolidated.js --create-topics --ctp 2>&1 | tee "$testresultConfluentCtp"
+MODE=kafkajs MESSAGE_COUNT=5000 node performance-consolidated.js --create-topics --ctp 2>&1 | tee "$testresultKjsCtp"
+
+producerConfluent=$(grep "=== Producer Rate:" "$testresultConfluentProducerConsumer" | cut -d':' -f2 | tr -d ' ')
+consumerConfluent=$(grep "=== Consumer Rate:" "$testresultConfluentProducerConsumer" | cut -d':' -f2 | tr -d ' ')
+ctpConfluent=$(grep "=== Consume-Transform-Produce Rate:" "$testresultConfluentCtp" | cut -d':' -f2 | tr -d ' ')
+producerKjs=$(grep "=== Producer Rate:" "$testresultKjsProducerConsumer" | cut -d':' -f2 | tr -d ' ')
+consumerKjs=$(grep "=== Consumer Rate:" "$testresultKjsProducerConsumer" | cut -d':' -f2 | tr -d ' ')
+ctpKjs=$(grep "=== Consume-Transform-Produce Rate:" "$testresultKjsCtp" | cut -d':' -f2 | tr -d ' ')
+
+echo "Producer rates: confluent $producerConfluent, kafkajs $producerKjs"
+echo "Consumer rates: confluent $consumerConfluent, kafkajs $consumerKjs"
+echo "CTP rates: confluent $ctpConfluent, kafkajs $ctpKjs"
+
+errcode=0
+
+# Compare against KJS
+if [[ $(echo "$producerConfluent < $producerKjs * 70 / 100" | bc -l) -eq 1 ]]; then
+ echo "Producer rates differ by more than 30%: confluent $producerConfluent, kafkajs $producerKjs"
+ errcode=1
+fi
+
+if [[ $(echo "$consumerConfluent < $consumerKjs * 70 / 100" | bc -l) -eq 1 ]]; then
+ echo "Consumer rates differ by more than 30%: confluent $consumerConfluent, kafkajs $consumerKjs"
+ # FIXME: improve consumer performance at least to KafkaJS level
+ errcode=0
+fi
+
+if [[ $(echo "$ctpConfluent < $ctpKjs * 70 / 100" | bc -l) -eq 1 ]]; then
+ echo "CTP rates differ by more than 30%: confluent $ctpConfluent, kafkajs $ctpKjs"
+ errcode=1
+fi
+
+# Compare against numbers set within semaphore config
+TARGET_PRODUCE="${TARGET_PRODUCE_PERFORMANCE:-35}"
+TARGET_CONSUME="${TARGET_CONSUME_PERFORMANCE:-18}"
+TARGET_CTP="${TARGET_CTP_PERFORMANCE:-0.02}"
+
+if [[ $(echo "$producerConfluent < $TARGET_PRODUCE" | bc -l) -eq 1 ]]; then
+ echo "Confluent producer rate is below target: $producerConfluent"
+ errcode=1
+fi
+
+if [[ $(echo "$consumerConfluent < $TARGET_CONSUME" | bc -l) -eq 1 ]]; then
+ echo "Confluent consumer rate is below target: $consumerConfluent"
+ errcode=1
+fi
+
+if [[ $(echo "$ctpConfluent < $TARGET_CTP" | bc -l) -eq 1 ]]; then
+ echo "Confluent CTP rate is below target: $ctpConfluent"
+ errcode=1
+fi
+
+exit $errcode
+
diff --git a/ci/update-version.js b/ci/update-version.js
index 4265ea9b..50957b74 100644
--- a/ci/update-version.js
+++ b/ci/update-version.js
@@ -84,24 +84,15 @@ function getBranch(cb) {
}
function getPackageVersion(tag, branch) {
- const baseVersion = `v${tag.major}.${tag.minor}.${tag.patch}`;
+ let baseVersion = `v${tag.major}.${tag.minor}.${tag.patch}`;
- console.log(`Package version is "${baseVersion}"`);
-
- // never publish with an suffix
- // fixes https://github.com/confluentinc/confluent-kafka-js/issues/981
- // baseVersion += '-';
-
- // if (tag.commit === 0 && branch === 'master') {
- // return baseVersion;
- // }
- // if (branch !== 'master') {
- // baseVersion += (tag.commit + 1 + '.' + branch);
- // } else {
- // baseVersion += (tag.commit + 1);
- // }
+ // publish with a -devel suffix for EA and RC releases.
+ if (tag.prerelease.length > 0) {
+ baseVersion += '-' + tag.prerelease.join('-');
+ }
+ console.log(`Package version is "${baseVersion}"`);
return baseVersion;
}
@@ -114,7 +105,6 @@ getVersion((err, tag) => {
if (err) {
throw err;
}
-
pjs.version = getPackageVersion(tag, branch);
fs.writeFileSync(pjsPath, JSON.stringify(pjs, null, 2));
diff --git a/cpplint.py b/cpplint.py
index 44726248..0e50a882 100644
--- a/cpplint.py
+++ b/cpplint.py
@@ -42,84 +42,155 @@
"""
import codecs
+import collections
import copy
import getopt
+import glob
+import itertools
import math # for log
import os
import re
-import sre_compile
import string
import sys
+import sysconfig
import unicodedata
+import xml.etree.ElementTree
+# if empty, use defaults
+_valid_extensions = set([])
+
+__VERSION__ = '1.7'
_USAGE = """
-Syntax: cpplint.py [--verbose=#] [--output=vs7] [--filter=-x,+y,...]
+Syntax: cpplint.py [--verbose=#] [--output=emacs|eclipse|vs7|junit|sed|gsed]
+ [--filter=-x,+y,...]
[--counting=total|toplevel|detailed] [--root=subdir]
- [--linelength=digits]
+ [--repository=path]
+ [--linelength=digits] [--headers=x,y,...]
+ [--recursive]
+ [--exclude=path]
+ [--extensions=hpp,cpp,...]
+ [--includeorder=default|standardcfirst]
+ [--config=filename]
+ [--quiet]
+ [--version]
[file] ...
+ Style checker for C/C++ source files.
+ This is a fork of the Google style checker with minor extensions.
+
The style guidelines this tries to follow are those in
- http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml
+ https://google.github.io/styleguide/cppguide.html
Every problem is given a confidence score from 1-5, with 5 meaning we are
certain of the problem, and 1 meaning it could be a legitimate construct.
This will miss some errors, and is not a substitute for a code review.
- To suppress false-positive errors of a certain category, add a
- 'NOLINT(category)' comment to the line. NOLINT or NOLINT(*)
- suppresses errors of all categories on that line.
+ To suppress false-positive errors of certain categories, add a
+ 'NOLINT(category[, category...])' comment to the line. NOLINT or NOLINT(*)
+ suppresses errors of all categories on that line. To suppress categories
+ on the next line use NOLINTNEXTLINE instead of NOLINT. To suppress errors in
+ a block of code 'NOLINTBEGIN(category[, category...])' comment to a line at
+ the start of the block and to end the block add a comment with 'NOLINTEND'.
+ NOLINT blocks are inclusive so any statements on the same line as a BEGIN
+ or END will have the error suppression applied.
The files passed in will be linted; at least one file must be provided.
- Default linted extensions are .cc, .cpp, .cu, .cuh and .h. Change the
- extensions with the --extensions flag.
+ Default linted extensions are %s.
+ Other file types will be ignored.
+ Change the extensions with the --extensions flag.
Flags:
- output=vs7
+ output=emacs|eclipse|vs7|junit|sed|gsed
By default, the output is formatted to ease emacs parsing. Visual Studio
- compatible output (vs7) may also be used. Other formats are unsupported.
+ compatible output (vs7) may also be used. Further support exists for
+ eclipse (eclipse), and JUnit (junit). XML parsers such as those used
+ in Jenkins and Bamboo may also be used.
+ The sed format outputs sed commands that should fix some of the errors.
+ Note that this requires gnu sed. If that is installed as gsed on your
+ system (common e.g. on macOS with homebrew) you can use the gsed output
+ format. Sed commands are written to stdout, not stderr, so you should be
+ able to pipe output straight to a shell to run the fixes.
verbose=#
Specify a number 0-5 to restrict errors to certain verbosity levels.
+ Errors with lower verbosity levels have lower confidence and are more
+ likely to be false positives.
+
+ quiet
+ Don't print anything if no errors are found.
filter=-x,+y,...
Specify a comma-separated list of category-filters to apply: only
error messages whose category names pass the filters will be printed.
(Category names are printed with the message and look like
"[whitespace/indent]".) Filters are evaluated left to right.
- "-FOO" and "FOO" means "do not print categories that start with FOO".
+ "-FOO" means "do not print categories that start with FOO".
"+FOO" means "do print categories that start with FOO".
Examples: --filter=-whitespace,+whitespace/braces
- --filter=whitespace,runtime/printf,+runtime/printf_format
+ --filter=-whitespace,-runtime/printf,+runtime/printf_format
--filter=-,+build/include_what_you_use
To see a list of all the categories used in cpplint, pass no arg:
--filter=
+ Filters can directly be limited to files and also line numbers. The
+ syntax is category:file:line , where line is optional. The filter limitation
+ works for both + and - and can be combined with ordinary filters:
+
+ Examples: --filter=-whitespace:foo.h,+whitespace/braces:foo.h
+ --filter=-whitespace,-runtime/printf:foo.h:14,+runtime/printf_format:foo.h
+ --filter=-,+build/include_what_you_use:foo.h:321
+
counting=total|toplevel|detailed
The total number of errors found is always printed. If
'toplevel' is provided, then the count of errors in each of
the top-level categories like 'build' and 'whitespace' will
also be printed. If 'detailed' is provided, then a count
- is provided for each category like 'build/class'.
+ is provided for each category like 'legal/copyright'.
+
+ repository=path
+ The top level directory of the repository, used to derive the header
+ guard CPP variable. By default, this is determined by searching for a
+ path that contains .git, .hg, or .svn. When this flag is specified, the
+ given path is used instead. This option allows the header guard CPP
+ variable to remain consistent even if members of a team have different
+ repository root directories (such as when checking out a subdirectory
+ with SVN). In addition, users of non-mainstream version control systems
+ can use this flag to ensure readable header guard CPP variables.
+
+ Examples:
+ Assuming that Alice checks out ProjectName and Bob checks out
+ ProjectName/trunk and trunk contains src/chrome/ui/browser.h, then
+ with no --repository flag, the header guard CPP variable will be:
+
+ Alice => TRUNK_SRC_CHROME_BROWSER_UI_BROWSER_H_
+ Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_
+
+ If Alice uses the --repository=trunk flag and Bob omits the flag or
+ uses --repository=. then the header guard CPP variable will be:
+
+ Alice => SRC_CHROME_BROWSER_UI_BROWSER_H_
+ Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_
root=subdir
The root directory used for deriving header guard CPP variable.
- By default, the header guard CPP variable is calculated as the relative
- path to the directory that contains .git, .hg, or .svn. When this flag
- is specified, the relative path is calculated from the specified
- directory. If the specified directory does not exist, this flag is
- ignored.
+ This directory is relative to the top level directory of the repository
+ which by default is determined by searching for a directory that contains
+ .git, .hg, or .svn but can also be controlled with the --repository flag.
+ If the specified directory does not exist, this flag is ignored.
Examples:
- Assuming that src/.git exists, the header guard CPP variables for
+ Assuming that src is the top level directory of the repository (and
+ cwd=top/src), the header guard CPP variables for
src/chrome/browser/ui/browser.h are:
No flag => CHROME_BROWSER_UI_BROWSER_H_
--root=chrome => BROWSER_UI_BROWSER_H_
--root=chrome/browser => UI_BROWSER_H_
+ --root=.. => SRC_CHROME_BROWSER_UI_BROWSER_H_
linelength=digits
This is the allowed line length for the project. The default value is
@@ -128,11 +199,50 @@
Examples:
--linelength=120
+ recursive
+ Search for files to lint recursively. Each directory given in the list
+ of files to be linted is replaced by all files that descend from that
+ directory. Files with extensions not in the valid extensions list are
+ excluded.
+
+ exclude=path
+ Exclude the given path from the list of files to be linted. Relative
+ paths are evaluated relative to the current directory and shell globbing
+ is performed. This flag can be provided multiple times to exclude
+ multiple files.
+
+ Examples:
+ --exclude=one.cc
+ --exclude=src/*.cc
+ --exclude=src/*.cc --exclude=test/*.cc
+
extensions=extension,extension,...
The allowed file extensions that cpplint will check
Examples:
- --extensions=hpp,cpp
+ --extensions=%s
+
+ includeorder=default|standardcfirst
+ For the build/include_order rule, the default is to blindly assume angle
+ bracket includes with file extension are c-system-headers (default),
+ even knowing this will have false classifications.
+ The default is established at google.
+ standardcfirst means to instead use an allow-list of known c headers and
+ treat all others as separate group of "other system headers". The C headers
+ included are those of the C-standard lib and closely related ones.
+
+ config=filename
+ Search for config files with the specified name instead of CPPLINT.cfg
+
+ headers=x,y,...
+ The header extensions that cpplint will treat as .h in checks. Values are
+ automatically added to --extensions list.
+ (by default, only files with extensions %s will be assumed to be headers)
+
+ Examples:
+ --headers=%s
+ --headers=hpp,hxx
+ --headers=hpp
cpplint.py supports per-directory configurations specified in CPPLINT.cfg
files. CPPLINT.cfg file can contain a number of key=value pairs.
@@ -142,6 +252,8 @@
filter=+filter1,-filter2,...
exclude_files=regex
linelength=80
+ root=subdir
+ headers=x,y,...
"set noparent" option prevents cpplint from traversing directory tree
upwards looking for more .cfg files in parent directories. This option
@@ -153,16 +265,22 @@
"exclude_files" allows to specify a regular expression to be matched against
a file name. If the expression matches, the file is skipped and not run
- through liner.
+ through the linter.
"linelength" allows to specify the allowed line length for the project.
+ The "root" option is similar in function to the --root flag (see example
+ above). Paths are relative to the directory of the CPPLINT.cfg.
+
+ The "headers" option is similar in function to the --headers flag
+ (see example above).
+
CPPLINT.cfg has an effect on files in the same directory and all
sub-directories, unless overridden by a nested configuration file.
Example file:
filter=-build/include_order,+build/include_alpha
- exclude_files=.*\.cc
+ exclude_files=.*\\.cc
The above example disables build/include_order warning and enables
build/include_alpha as well as excludes all .cc from being
@@ -175,17 +293,20 @@
# If you add a new error message with a new category, add it to the list
# here! cpplint_unittest.py should tell you if you forget to do this.
_ERROR_CATEGORIES = [
- 'build/class',
'build/c++11',
+ 'build/c++17',
'build/deprecated',
'build/endif_comment',
'build/explicit_make_pair',
'build/forward_decl',
'build/header_guard',
'build/include',
+ 'build/include_subdir',
'build/include_alpha',
'build/include_order',
'build/include_what_you_use',
+ 'build/namespaces_headers',
+ 'build/namespaces_literals',
'build/namespaces',
'build/printf_format',
'build/storage_class',
@@ -196,7 +317,6 @@
'readability/check',
'readability/constructors',
'readability/fn_size',
- 'readability/function',
'readability/inheritance',
'readability/multiline_comment',
'readability/multiline_string',
@@ -214,7 +334,6 @@
'runtime/invalid_increment',
'runtime/member_string_references',
'runtime/memset',
- 'runtime/indentation_namespace',
'runtime/operator',
'runtime/printf',
'runtime/printf_format',
@@ -227,11 +346,13 @@
'whitespace/comma',
'whitespace/comments',
'whitespace/empty_conditional_body',
+ 'whitespace/empty_if_body',
'whitespace/empty_loop_body',
'whitespace/end_of_line',
'whitespace/ending_newline',
'whitespace/forcolon',
'whitespace/indent',
+ 'whitespace/indent_namespace',
'whitespace/line_length',
'whitespace/newline',
'whitespace/operators',
@@ -241,10 +362,49 @@
'whitespace/todo',
]
+# keywords to use with --outputs which generate stdout for machine processing
+_MACHINE_OUTPUTS = [
+ 'junit',
+ 'sed',
+ 'gsed'
+]
+
# These error categories are no longer enforced by cpplint, but for backwards-
# compatibility they may still appear in NOLINT comments.
_LEGACY_ERROR_CATEGORIES = [
+ 'build/class',
'readability/streams',
+ 'readability/function',
+ ]
+
+# These prefixes for categories should be ignored since they relate to other
+# tools which also use the NOLINT syntax, e.g. clang-tidy.
+_OTHER_NOLINT_CATEGORY_PREFIXES = [
+ 'clang-analyzer-',
+ 'abseil-',
+ 'altera-',
+ 'android-',
+ 'boost-',
+ 'bugprone-',
+ 'cert-',
+ 'concurrency-',
+ 'cppcoreguidelines-',
+ 'darwin-',
+ 'fuchsia-',
+ 'google-',
+ 'hicpp-',
+ 'linuxkernel-',
+ 'llvm-',
+ 'llvmlibc-',
+ 'misc-',
+ 'modernize-',
+ 'mpi-',
+ 'objc-',
+ 'openmp-',
+ 'performance-',
+ 'portability-',
+ 'readability-',
+ 'zircon-',
]
# The default state of the category filter. This is overridden by the --filter=
@@ -253,6 +413,16 @@
# All entries here should start with a '-' or '+', as in the --filter= flag.
_DEFAULT_FILTERS = ['-build/include_alpha']
+# The default list of categories suppressed for C (not C++) files.
+_DEFAULT_C_SUPPRESSED_CATEGORIES = [
+ 'readability/casting',
+ ]
+
+# The default list of categories suppressed for Linux Kernel files.
+_DEFAULT_KERNEL_SUPPRESSED_CATEGORIES = [
+ 'whitespace/tab',
+ ]
+
# We used to check for high-bit characters, but after much discussion we
# decided those were OK, as long as they were in UTF-8 and didn't represent
# hard-coded international strings, which belong in a separate i18n file.
@@ -265,7 +435,7 @@
'alloc.h',
'builtinbuf.h',
'bvector.h',
- 'complex.h',
+ # 'complex.h', collides with System C header "complex.h" since C11
'defalloc.h',
'deque.h',
'editbuf.h',
@@ -311,7 +481,7 @@
'tree.h',
'type_traits.h',
'vector.h',
- # 17.6.1.2 C++ library headers
+ # C++ library headers
'algorithm',
'array',
'atomic',
@@ -346,6 +516,7 @@
'random',
'ratio',
'regex',
+ 'scoped_allocator',
'set',
'sstream',
'stack',
@@ -364,7 +535,45 @@
'utility',
'valarray',
'vector',
- # 17.6.1.2 C++ headers for C library facilities
+ # C++14 headers
+ 'shared_mutex',
+ # C++17 headers
+ 'any',
+ 'charconv',
+ 'codecvt',
+ 'execution',
+ 'filesystem',
+ 'memory_resource',
+ 'optional',
+ 'string_view',
+ 'variant',
+ # C++20 headers
+ 'barrier',
+ 'bit',
+ 'compare',
+ 'concepts',
+ 'coroutine',
+ 'format',
+ 'latch'
+ 'numbers',
+ 'ranges',
+ 'semaphore',
+ 'source_location',
+ 'span',
+ 'stop_token',
+ 'syncstream',
+ 'version',
+ # C++23 headers
+ 'expected',
+ 'flat_map',
+ 'flat_set',
+ 'generator',
+ 'mdspan',
+ 'print',
+ 'spanstream',
+ 'stacktrace',
+ 'stdfloat',
+ # C++ headers for C library facilities
'cassert',
'ccomplex',
'cctype',
@@ -393,49 +602,242 @@
'cwctype',
])
+# C headers
+_C_HEADERS = frozenset([
+ # System C headers
+ 'assert.h',
+ 'complex.h',
+ 'ctype.h',
+ 'errno.h',
+ 'fenv.h',
+ 'float.h',
+ 'inttypes.h',
+ 'iso646.h',
+ 'limits.h',
+ 'locale.h',
+ 'math.h',
+ 'setjmp.h',
+ 'signal.h',
+ 'stdalign.h',
+ 'stdarg.h',
+ 'stdatomic.h',
+ 'stdbool.h',
+ 'stddef.h',
+ 'stdint.h',
+ 'stdio.h',
+ 'stdlib.h',
+ 'stdnoreturn.h',
+ 'string.h',
+ 'tgmath.h',
+ 'threads.h',
+ 'time.h',
+ 'uchar.h',
+ 'wchar.h',
+ 'wctype.h',
+ # C23 headers
+ 'stdbit.h',
+ 'stdckdint.h',
+ # additional POSIX C headers
+ 'aio.h',
+ 'arpa/inet.h',
+ 'cpio.h',
+ 'dirent.h',
+ 'dlfcn.h',
+ 'fcntl.h',
+ 'fmtmsg.h',
+ 'fnmatch.h',
+ 'ftw.h',
+ 'glob.h',
+ 'grp.h',
+ 'iconv.h',
+ 'langinfo.h',
+ 'libgen.h',
+ 'monetary.h',
+ 'mqueue.h',
+ 'ndbm.h',
+ 'net/if.h',
+ 'netdb.h',
+ 'netinet/in.h',
+ 'netinet/tcp.h',
+ 'nl_types.h',
+ 'poll.h',
+ 'pthread.h',
+ 'pwd.h',
+ 'regex.h',
+ 'sched.h',
+ 'search.h',
+ 'semaphore.h',
+ 'setjmp.h',
+ 'signal.h',
+ 'spawn.h',
+ 'strings.h',
+ 'stropts.h',
+ 'syslog.h',
+ 'tar.h',
+ 'termios.h',
+ 'trace.h',
+ 'ulimit.h',
+ 'unistd.h',
+ 'utime.h',
+ 'utmpx.h',
+ 'wordexp.h',
+ # additional GNUlib headers
+ 'a.out.h',
+ 'aliases.h',
+ 'alloca.h',
+ 'ar.h',
+ 'argp.h',
+ 'argz.h',
+ 'byteswap.h',
+ 'crypt.h',
+ 'endian.h',
+ 'envz.h',
+ 'err.h',
+ 'error.h',
+ 'execinfo.h',
+ 'fpu_control.h',
+ 'fstab.h',
+ 'fts.h',
+ 'getopt.h',
+ 'gshadow.h',
+ 'ieee754.h',
+ 'ifaddrs.h',
+ 'libintl.h',
+ 'mcheck.h',
+ 'mntent.h',
+ 'obstack.h',
+ 'paths.h',
+ 'printf.h',
+ 'pty.h',
+ 'resolv.h',
+ 'shadow.h',
+ 'sysexits.h',
+ 'ttyent.h',
+ # Additional linux glibc headers
+ 'dlfcn.h',
+ 'elf.h',
+ 'features.h',
+ 'gconv.h',
+ 'gnu-versions.h',
+ 'lastlog.h',
+ 'libio.h',
+ 'link.h',
+ 'malloc.h',
+ 'memory.h',
+ 'netash/ash.h',
+ 'netatalk/at.h',
+ 'netax25/ax25.h',
+ 'neteconet/ec.h',
+ 'netipx/ipx.h',
+ 'netiucv/iucv.h',
+ 'netpacket/packet.h',
+ 'netrom/netrom.h',
+ 'netrose/rose.h',
+ 'nfs/nfs.h',
+ 'nl_types.h',
+ 'nss.h',
+ 're_comp.h',
+ 'regexp.h',
+ 'sched.h',
+ 'sgtty.h',
+ 'stab.h',
+ 'stdc-predef.h',
+ 'stdio_ext.h',
+ 'syscall.h',
+ 'termio.h',
+ 'thread_db.h',
+ 'ucontext.h',
+ 'ustat.h',
+ 'utmp.h',
+ 'values.h',
+ 'wait.h',
+ 'xlocale.h',
+ # Hardware specific headers
+ 'arm_neon.h',
+ 'emmintrin.h',
+ 'xmmintin.h',
+ ])
+
+# Folders of C libraries so commonly used in C++,
+# that they have parity with standard C libraries.
+C_STANDARD_HEADER_FOLDERS = frozenset([
+ # standard C library
+ "sys",
+ # glibc for linux
+ "arpa",
+ "asm-generic",
+ "bits",
+ "gnu",
+ "net",
+ "netinet",
+ "protocols",
+ "rpc",
+ "rpcsvc",
+ "scsi",
+ # linux kernel header
+ "drm",
+ "linux",
+ "misc",
+ "mtd",
+ "rdma",
+ "sound",
+ "video",
+ "xen",
+ ])
+
+# Type names
+_TYPES = re.compile(
+ r'^(?:'
+ # [dcl.type.simple]
+ r'(char(16_t|32_t)?)|wchar_t|'
+ r'bool|short|int|long|signed|unsigned|float|double|'
+ # [support.types]
+ r'(ptrdiff_t|size_t|max_align_t|nullptr_t)|'
+ # [cstdint.syn]
+ r'(u?int(_fast|_least)?(8|16|32|64)_t)|'
+ r'(u?int(max|ptr)_t)|'
+ r')$')
+
# These headers are excluded from [build/include] and [build/include_order]
# checks:
# - Anything not following google file name conventions (containing an
# uppercase character, such as Python.h or nsStringAPI.h, for example).
# - Lua headers.
-# - rdkafka.cpp header, because it would be located in different directories depending
-# on whether it's pulled from librdkafka sources or librdkafka-dev package.
_THIRD_PARTY_HEADERS_PATTERN = re.compile(
- r'^(?:[^/]*[A-Z][^/]*\.h|lua\.h|lauxlib\.h|lualib\.h|rdkafkacpp\.h)$')
+ r'^(?:[^/]*[A-Z][^/]*\.h|lua\.h|lauxlib\.h|lualib\.h)$')
+
+# Pattern for matching FileInfo.BaseName() against test file name
+_test_suffixes = ['_test', '_regtest', '_unittest']
+_TEST_FILE_SUFFIX = '(' + '|'.join(_test_suffixes) + r')$'
+# Pattern that matches only complete whitespace, possibly across multiple lines.
+_EMPTY_CONDITIONAL_BODY_PATTERN = re.compile(r'^\s*$', re.DOTALL)
# Assertion macros. These are defined in base/logging.h and
-# testing/base/gunit.h. Note that the _M versions need to come first
-# for substring matching to work.
+# testing/base/public/gunit.h.
_CHECK_MACROS = [
'DCHECK', 'CHECK',
- 'EXPECT_TRUE_M', 'EXPECT_TRUE',
- 'ASSERT_TRUE_M', 'ASSERT_TRUE',
- 'EXPECT_FALSE_M', 'EXPECT_FALSE',
- 'ASSERT_FALSE_M', 'ASSERT_FALSE',
+ 'EXPECT_TRUE', 'ASSERT_TRUE',
+ 'EXPECT_FALSE', 'ASSERT_FALSE',
]
# Replacement macros for CHECK/DCHECK/EXPECT_TRUE/EXPECT_FALSE
-_CHECK_REPLACEMENT = dict([(m, {}) for m in _CHECK_MACROS])
+_CHECK_REPLACEMENT = dict([(macro_var, {}) for macro_var in _CHECK_MACROS])
for op, replacement in [('==', 'EQ'), ('!=', 'NE'),
('>=', 'GE'), ('>', 'GT'),
('<=', 'LE'), ('<', 'LT')]:
- _CHECK_REPLACEMENT['DCHECK'][op] = 'DCHECK_%s' % replacement
- _CHECK_REPLACEMENT['CHECK'][op] = 'CHECK_%s' % replacement
- _CHECK_REPLACEMENT['EXPECT_TRUE'][op] = 'EXPECT_%s' % replacement
- _CHECK_REPLACEMENT['ASSERT_TRUE'][op] = 'ASSERT_%s' % replacement
- _CHECK_REPLACEMENT['EXPECT_TRUE_M'][op] = 'EXPECT_%s_M' % replacement
- _CHECK_REPLACEMENT['ASSERT_TRUE_M'][op] = 'ASSERT_%s_M' % replacement
+ _CHECK_REPLACEMENT['DCHECK'][op] = f'DCHECK_{replacement}'
+ _CHECK_REPLACEMENT['CHECK'][op] = f'CHECK_{replacement}'
+ _CHECK_REPLACEMENT['EXPECT_TRUE'][op] = f'EXPECT_{replacement}'
+ _CHECK_REPLACEMENT['ASSERT_TRUE'][op] = f'ASSERT_{replacement}'
for op, inv_replacement in [('==', 'NE'), ('!=', 'EQ'),
('>=', 'LT'), ('>', 'LE'),
('<=', 'GT'), ('<', 'GE')]:
- _CHECK_REPLACEMENT['EXPECT_FALSE'][op] = 'EXPECT_%s' % inv_replacement
- _CHECK_REPLACEMENT['ASSERT_FALSE'][op] = 'ASSERT_%s' % inv_replacement
- _CHECK_REPLACEMENT['EXPECT_FALSE_M'][op] = 'EXPECT_%s_M' % inv_replacement
- _CHECK_REPLACEMENT['ASSERT_FALSE_M'][op] = 'ASSERT_%s_M' % inv_replacement
+ _CHECK_REPLACEMENT['EXPECT_FALSE'][op] = f'EXPECT_{inv_replacement}'
+ _CHECK_REPLACEMENT['ASSERT_FALSE'][op] = f'ASSERT_{inv_replacement}'
# Alternative tokens and their replacements. For full list, see section 2.5
# Alternative tokens [lex.digraph] in the C++ standard.
@@ -462,16 +864,17 @@
# False positives include C-style multi-line comments and multi-line strings
# but those have always been troublesome for cpplint.
_ALT_TOKEN_REPLACEMENT_PATTERN = re.compile(
- r'[ =()](' + ('|'.join(_ALT_TOKEN_REPLACEMENT.keys())) + r')(?=[ (]|$)')
+ r'([ =()])(' + ('|'.join(_ALT_TOKEN_REPLACEMENT.keys())) + r')([ (]|$)')
# These constants define types of headers for use with
# _IncludeState.CheckNextIncludeOrder().
_C_SYS_HEADER = 1
_CPP_SYS_HEADER = 2
-_LIKELY_MY_HEADER = 3
-_POSSIBLE_MY_HEADER = 4
-_OTHER_HEADER = 5
+_OTHER_SYS_HEADER = 3
+_LIKELY_MY_HEADER = 4
+_POSSIBLE_MY_HEADER = 5
+_OTHER_HEADER = 6
# These constants define the current inline assembly state
_NO_ASM = 0 # Outside of inline assembly block
@@ -484,8 +887,28 @@
r'(?:\s+(volatile|__volatile__))?'
r'\s*[{(]')
-
-_regexp_compile_cache = {}
+# Match strings that indicate we're working on a C (not C++) file.
+_SEARCH_C_FILE = re.compile(r'\b(?:LINT_C_FILE|'
+ r'vim?:\s*.*(\s*|:)filetype=c(\s*|:|$))')
+
+# Match string that indicates we're working on a Linux Kernel file.
+_SEARCH_KERNEL_FILE = re.compile(r'\b(?:LINT_KERNEL_FILE)')
+
+# Commands for sed to fix the problem
+_SED_FIXUPS = {
+ 'Remove spaces around =': r's/ = /=/',
+ 'Remove spaces around !=': r's/ != /!=/',
+ 'Remove space before ( in if (': r's/if (/if(/',
+ 'Remove space before ( in for (': r's/for (/for(/',
+ 'Remove space before ( in while (': r's/while (/while(/',
+ 'Remove space before ( in switch (': r's/switch (/switch(/',
+ 'Should have a space between // and comment': r's/\/\//\/\/ /',
+ 'Missing space before {': r's/\([^ ]\){/\1 {/',
+ 'Tab found, replace by spaces': r's/\t/ /g',
+ 'Line ends in whitespace. Consider deleting these extra spaces.': r's/\s*$//',
+ 'You don\'t need a ; after a }': r's/};/}/',
+ 'Missing space after ,': r's/,\([^ ]\)/, \1/g',
+}
# {str, set(int)}: a map from error categories to sets of linenumbers
# on which those errors are expected and should be suppressed.
@@ -494,17 +917,147 @@
# The root directory used for deriving header guard CPP variable.
# This is set by --root flag.
_root = None
+_root_debug = False
+
+# The top level repository directory. If set, _root is calculated relative to
+# this directory instead of the directory containing version control artifacts.
+# This is set by the --repository flag.
+_repository = None
+
+# Files to exclude from linting. This is set by the --exclude flag.
+_excludes = None
+
+# Whether to suppress all PrintInfo messages, UNRELATED to --quiet flag
+_quiet = False
# The allowed line length of files.
# This is set by --linelength flag.
_line_length = 80
+# This allows to use different include order rule than default
+_include_order = "default"
+
+# This allows different config files to be used
+_config_filename = "CPPLINT.cfg"
+
+# Treat all headers starting with 'h' equally: .h, .hpp, .hxx etc.
+# This is set by --headers flag.
+_hpp_headers = set([])
+
+class ErrorSuppressions:
+ """Class to track all error suppressions for cpplint"""
+
+ class LineRange:
+ """Class to represent a range of line numbers for which an error is suppressed"""
+ def __init__(self, begin, end):
+ self.begin = begin
+ self.end = end
+
+ def __str__(self):
+ return f'[{self.begin}-{self.end}]'
+
+ def __contains__(self, obj):
+ return self.begin <= obj <= self.end
+
+ def ContainsRange(self, other):
+ return self.begin <= other.begin and self.end >= other.end
+
+ def __init__(self):
+ self._suppressions = collections.defaultdict(list)
+ self._open_block_suppression = None
+
+ def _AddSuppression(self, category, line_range):
+ suppressed = self._suppressions[category]
+ if not (suppressed and suppressed[-1].ContainsRange(line_range)):
+ suppressed.append(line_range)
+
+ def GetOpenBlockStart(self):
+ """:return: The start of the current open block or `-1` if there is not an open block"""
+ return self._open_block_suppression.begin if self._open_block_suppression else -1
+
+ def AddGlobalSuppression(self, category):
+ """Add a suppression for `category` which is suppressed for the whole file"""
+ self._AddSuppression(category, self.LineRange(0, math.inf))
+
+ def AddLineSuppression(self, category, linenum):
+ """Add a suppression for `category` which is suppressed only on `linenum`"""
+ self._AddSuppression(category, self.LineRange(linenum, linenum))
+
+ def StartBlockSuppression(self, category, linenum):
+ """Start a suppression block for `category` on `linenum`. inclusive"""
+ if self._open_block_suppression is None:
+ self._open_block_suppression = self.LineRange(linenum, math.inf)
+ self._AddSuppression(category, self._open_block_suppression)
+
+ def EndBlockSuppression(self, linenum):
+ """End the current block suppression on `linenum`. inclusive"""
+ if self._open_block_suppression:
+ self._open_block_suppression.end = linenum
+ self._open_block_suppression = None
+
+ def IsSuppressed(self, category, linenum):
+ """:return: `True` if `category` is suppressed for `linenum`"""
+ suppressed = self._suppressions[category] + self._suppressions[None]
+ return any(linenum in lr for lr in suppressed)
+
+ def HasOpenBlock(self):
+ """:return: `True` if a block suppression was started but not ended"""
+ return self._open_block_suppression is not None
+
+ def Clear(self):
+ """Clear all current error suppressions"""
+ self._suppressions.clear()
+ self._open_block_suppression = None
+
+_error_suppressions = ErrorSuppressions()
+
+def ProcessHppHeadersOption(val):
+ global _hpp_headers
+ try:
+ _hpp_headers = {ext.strip() for ext in val.split(',')}
+ except ValueError:
+ PrintUsage('Header extensions must be comma separated list.')
+
+def ProcessIncludeOrderOption(val):
+ if val is None or val == "default":
+ pass
+ elif val == "standardcfirst":
+ global _include_order
+ _include_order = val
+ else:
+ PrintUsage('Invalid includeorder value %s. Expected default|standardcfirst')
+
+def IsHeaderExtension(file_extension):
+ return file_extension in GetHeaderExtensions()
+
+def GetHeaderExtensions():
+ if _hpp_headers:
+ return _hpp_headers
+ if _valid_extensions:
+ return {h for h in _valid_extensions if 'h' in h}
+ return set(['h', 'hh', 'hpp', 'hxx', 'h++', 'cuh'])
+
# The allowed extensions for file names
-# This is set by --extensions flag.
-_valid_extensions = set(['cc', 'h', 'cpp', 'cu', 'cuh'])
+# This is set by --extensions flag
+def GetAllExtensions():
+ return GetHeaderExtensions().union(_valid_extensions or set(
+ ['c', 'cc', 'cpp', 'cxx', 'c++', 'cu']))
+
+def ProcessExtensionsOption(val):
+ global _valid_extensions
+ try:
+ extensions = [ext.strip() for ext in val.split(',')]
+ _valid_extensions = set(extensions)
+ except ValueError:
+ PrintUsage('Extensions should be a comma-separated list of values;'
+ 'for example: extensions=hpp,cpp\n'
+ f'This could not be parsed: "{val}"')
+
+def GetNonHeaderExtensions():
+ return GetAllExtensions().difference(GetHeaderExtensions())
def ParseNolintSuppressions(filename, raw_line, linenum, error):
- """Updates the global list of error-suppressions.
+ """Updates the global list of line error-suppressions.
Parses any NOLINT comments on the current line, updating the global
error_suppressions store. Reports an error if the NOLINT comment
@@ -516,79 +1069,91 @@ def ParseNolintSuppressions(filename, raw_line, linenum, error):
linenum: int, the number of the current line.
error: function, an error handler.
"""
- matched = Search(r'\bNOLINT(NEXTLINE)?\b(\([^)]+\))?', raw_line)
+ matched = re.search(r'\bNOLINT(NEXTLINE|BEGIN|END)?\b(\([^)]+\))?', raw_line)
if matched:
- if matched.group(1):
- suppressed_line = linenum + 1
- else:
- suppressed_line = linenum
- category = matched.group(2)
- if category in (None, '(*)'): # => "suppress all"
- _error_suppressions.setdefault(None, set()).add(suppressed_line)
+ no_lint_type = matched.group(1)
+ if no_lint_type == 'NEXTLINE':
+ def ProcessCategory(category):
+ _error_suppressions.AddLineSuppression(category, linenum + 1)
+ elif no_lint_type == 'BEGIN':
+ if _error_suppressions.HasOpenBlock():
+ error(filename, linenum, 'readability/nolint', 5,
+ f'NONLINT block already defined on line {_error_suppressions.GetOpenBlockStart()}')
+
+ def ProcessCategory(category):
+ _error_suppressions.StartBlockSuppression(category, linenum)
+ elif no_lint_type == 'END':
+ if not _error_suppressions.HasOpenBlock():
+ error(filename, linenum, 'readability/nolint', 5, 'Not in a NOLINT block')
+
+ def ProcessCategory(category):
+ if category is not None:
+ error(filename, linenum, 'readability/nolint', 5,
+ f'NOLINT categories not supported in block END: {category}')
+ _error_suppressions.EndBlockSuppression(linenum)
else:
- if category.startswith('(') and category.endswith(')'):
- category = category[1:-1]
+ def ProcessCategory(category):
+ _error_suppressions.AddLineSuppression(category, linenum)
+ categories = matched.group(2)
+ if categories in (None, '(*)'): # => "suppress all"
+ ProcessCategory(None)
+ elif categories.startswith('(') and categories.endswith(')'):
+ for category in set(map(lambda c: c.strip(), categories[1:-1].split(','))):
if category in _ERROR_CATEGORIES:
- _error_suppressions.setdefault(category, set()).add(suppressed_line)
+ ProcessCategory(category)
+ elif any(c for c in _OTHER_NOLINT_CATEGORY_PREFIXES if category.startswith(c)):
+ # Ignore any categories from other tools.
+ pass
elif category not in _LEGACY_ERROR_CATEGORIES:
error(filename, linenum, 'readability/nolint', 5,
- 'Unknown NOLINT error category: %s' % category)
+ f'Unknown NOLINT error category: {category}')
+
+def ProcessGlobalSuppresions(lines):
+ """Deprecated; use ProcessGlobalSuppressions."""
+ ProcessGlobalSuppressions(lines)
+
+def ProcessGlobalSuppressions(lines):
+ """Updates the list of global error suppressions.
+
+ Parses any lint directives in the file that have global effect.
+
+ Args:
+ lines: An array of strings, each representing a line of the file, with the
+ last element being empty if the file is terminated with a newline.
+ """
+ for line in lines:
+ if _SEARCH_C_FILE.search(line):
+ for category in _DEFAULT_C_SUPPRESSED_CATEGORIES:
+ _error_suppressions.AddGlobalSuppression(category)
+ if _SEARCH_KERNEL_FILE.search(line):
+ for category in _DEFAULT_KERNEL_SUPPRESSED_CATEGORIES:
+ _error_suppressions.AddGlobalSuppression(category)
def ResetNolintSuppressions():
"""Resets the set of NOLINT suppressions to empty."""
- _error_suppressions.clear()
+ _error_suppressions.Clear()
def IsErrorSuppressedByNolint(category, linenum):
"""Returns true if the specified error category is suppressed on this line.
Consults the global error_suppressions map populated by
- ParseNolintSuppressions/ResetNolintSuppressions.
+ ParseNolintSuppressions/ProcessGlobalSuppressions/ResetNolintSuppressions.
Args:
category: str, the category of the error.
linenum: int, the current line number.
Returns:
- bool, True iff the error should be suppressed due to a NOLINT comment.
- """
- return (linenum in _error_suppressions.get(category, set()) or
- linenum in _error_suppressions.get(None, set()))
-
-
-def Match(pattern, s):
- """Matches the string with the pattern, caching the compiled regexp."""
- # The regexp compilation caching is inlined in both Match and Search for
- # performance reasons; factoring it out into a separate function turns out
- # to be noticeably expensive.
- if pattern not in _regexp_compile_cache:
- _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
- return _regexp_compile_cache[pattern].match(s)
-
-
-def ReplaceAll(pattern, rep, s):
- """Replaces instances of pattern in a string with a replacement.
-
- The compiled regex is kept in a cache shared by Match and Search.
-
- Args:
- pattern: regex pattern
- rep: replacement text
- s: search string
-
- Returns:
- string with replacements made (or original string if no replacements)
+ bool, True iff the error should be suppressed due to a NOLINT comment,
+ block suppression or global suppression.
"""
- if pattern not in _regexp_compile_cache:
- _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
- return _regexp_compile_cache[pattern].sub(rep, s)
+ return _error_suppressions.IsSuppressed(category, linenum)
-def Search(pattern, s):
- """Searches the string for the pattern, caching the compiled regexp."""
- if pattern not in _regexp_compile_cache:
- _regexp_compile_cache[pattern] = sre_compile.compile(pattern)
- return _regexp_compile_cache[pattern].search(s)
+def _IsSourceExtension(s):
+ """File extension (excluding dot) matches a source file extension."""
+ return s in GetNonHeaderExtensions()
class _IncludeState(object):
@@ -609,11 +1174,13 @@ class _IncludeState(object):
_MY_H_SECTION = 1
_C_SECTION = 2
_CPP_SECTION = 3
- _OTHER_H_SECTION = 4
+ _OTHER_SYS_SECTION = 4
+ _OTHER_H_SECTION = 5
_TYPE_NAMES = {
_C_SYS_HEADER: 'C system header',
_CPP_SYS_HEADER: 'C++ system header',
+ _OTHER_SYS_HEADER: 'other system header',
_LIKELY_MY_HEADER: 'header this file implements',
_POSSIBLE_MY_HEADER: 'header this file may implement',
_OTHER_HEADER: 'other header',
@@ -623,11 +1190,14 @@ class _IncludeState(object):
_MY_H_SECTION: 'a header this file implements',
_C_SECTION: 'C system header',
_CPP_SECTION: 'C++ system header',
+ _OTHER_SYS_SECTION: 'other system header',
_OTHER_H_SECTION: 'other header',
}
def __init__(self):
self.include_list = [[]]
+ self._section = None
+ self._last_header = None
self.ResetSection('')
def FindHeader(self, header):
@@ -698,7 +1268,7 @@ def IsInAlphabeticalOrder(self, clean_lines, linenum, header_path):
# If previous line was a blank line, assume that the headers are
# intentionally sorted the way they are.
if (self._last_header > header_path and
- Match(r'^\s*#\s*include\b', clean_lines.elided[linenum - 1])):
+ re.match(r'^\s*#\s*include\b', clean_lines.elided[linenum - 1])):
return False
return True
@@ -716,9 +1286,8 @@ def CheckNextIncludeOrder(self, header_type):
error message describing what's wrong.
"""
- error_message = ('Found %s after %s' %
- (self._TYPE_NAMES[header_type],
- self._SECTION_NAMES[self._section]))
+ error_message = (f'Found {self._TYPE_NAMES[header_type]}'
+ f' after {self._SECTION_NAMES[self._section]}')
last_section = self._section
@@ -734,6 +1303,12 @@ def CheckNextIncludeOrder(self, header_type):
else:
self._last_header = ''
return error_message
+ elif header_type == _OTHER_SYS_HEADER:
+ if self._section <= self._OTHER_SYS_SECTION:
+ self._section = self._OTHER_SYS_SECTION
+ else:
+ self._last_header = ''
+ return error_message
elif header_type == _LIKELY_MY_HEADER:
if self._section <= self._MY_H_SECTION:
self._section = self._MY_H_SECTION
@@ -768,16 +1343,32 @@ def __init__(self):
self._filters_backup = self.filters[:]
self.counting = 'total' # In what way are we counting errors?
self.errors_by_category = {} # string to int dict storing error counts
+ self.quiet = False # Suppress non-error messagess?
# output format:
# "emacs" - format that emacs can parse (default)
+ # "eclipse" - format that eclipse can parse
# "vs7" - format that Microsoft Visual Studio 7 can parse
+ # "junit" - format that Jenkins, Bamboo, etc can parse
+ # "sed" - returns a gnu sed command to fix the problem
+ # "gsed" - like sed, but names the command gsed, e.g. for macOS homebrew users
self.output_format = 'emacs'
+ # For JUnit output, save errors and failures until the end so that they
+ # can be written into the XML
+ self._junit_errors = []
+ self._junit_failures = []
+
def SetOutputFormat(self, output_format):
"""Sets the output format for errors."""
self.output_format = output_format
+ def SetQuiet(self, quiet):
+ """Sets the module's quiet settings, and returns the previous setting."""
+ last_quiet = self.quiet
+ self.quiet = quiet
+ return last_quiet
+
def SetVerboseLevel(self, level):
"""Sets the module's verbosity, and returns the previous setting."""
last_verbose_level = self.verbose_level
@@ -815,7 +1406,7 @@ def AddFilters(self, filters):
for filt in self.filters:
if not (filt.startswith('+') or filt.startswith('-')):
raise ValueError('Every filter in --filters must start with + or -'
- ' (%s does not)' % filt)
+ f' ({filt} does not)')
def BackupFilters(self):
""" Saves the current filter list to backup storage."""
@@ -842,10 +1433,70 @@ def IncrementErrorCount(self, category):
def PrintErrorCounts(self):
"""Print a summary of errors by category, and the total."""
- for category, count in self.errors_by_category.iteritems():
- sys.stderr.write('Category \'%s\' errors found: %d\n' %
- (category, count))
- sys.stderr.write('Total errors found: %d\n' % self.error_count)
+ for category, count in sorted(dict.items(self.errors_by_category)):
+ self.PrintInfo(f'Category \'{category}\' errors found: {count}\n')
+ if self.error_count > 0:
+ self.PrintInfo(f'Total errors found: {self.error_count}\n')
+
+ def PrintInfo(self, message):
+ # _quiet does not represent --quiet flag.
+ # Hide infos from stdout to keep stdout pure for machine consumption
+ if not _quiet and self.output_format not in _MACHINE_OUTPUTS:
+ sys.stdout.write(message)
+
+ def PrintError(self, message):
+ if self.output_format == 'junit':
+ self._junit_errors.append(message)
+ else:
+ sys.stderr.write(message)
+
+ def AddJUnitFailure(self, filename, linenum, message, category, confidence):
+ self._junit_failures.append((filename, linenum, message, category,
+ confidence))
+
+ def FormatJUnitXML(self):
+ num_errors = len(self._junit_errors)
+ num_failures = len(self._junit_failures)
+
+ testsuite = xml.etree.ElementTree.Element('testsuite')
+ testsuite.attrib['errors'] = str(num_errors)
+ testsuite.attrib['failures'] = str(num_failures)
+ testsuite.attrib['name'] = 'cpplint'
+
+ if num_errors == 0 and num_failures == 0:
+ testsuite.attrib['tests'] = str(1)
+ xml.etree.ElementTree.SubElement(testsuite, 'testcase', name='passed')
+
+ else:
+ testsuite.attrib['tests'] = str(num_errors + num_failures)
+ if num_errors > 0:
+ testcase = xml.etree.ElementTree.SubElement(testsuite, 'testcase')
+ testcase.attrib['name'] = 'errors'
+ error = xml.etree.ElementTree.SubElement(testcase, 'error')
+ error.text = '\n'.join(self._junit_errors)
+ if num_failures > 0:
+ # Group failures by file
+ failed_file_order = []
+ failures_by_file = {}
+ for failure in self._junit_failures:
+ failed_file = failure[0]
+ if failed_file not in failed_file_order:
+ failed_file_order.append(failed_file)
+ failures_by_file[failed_file] = []
+ failures_by_file[failed_file].append(failure)
+ # Create a testcase for each file
+ for failed_file in failed_file_order:
+ failures = failures_by_file[failed_file]
+ testcase = xml.etree.ElementTree.SubElement(testsuite, 'testcase')
+ testcase.attrib['name'] = failed_file
+ failure = xml.etree.ElementTree.SubElement(testcase, 'failure')
+ template = '{0}: {1} [{2}] [{3}]'
+ texts = [template.format(f[1], f[2], f[3], f[4]) for f in failures]
+ failure.text = '\n'.join(texts)
+
+ xml_decl = '\n'
+ return xml_decl + xml.etree.ElementTree.tostring(testsuite, 'utf-8').decode('utf-8')
+
_cpplint_state = _CppLintState()
@@ -859,6 +1510,14 @@ def _SetOutputFormat(output_format):
"""Sets the module's output format."""
_cpplint_state.SetOutputFormat(output_format)
+def _Quiet():
+ """Return's the module's quiet setting."""
+ return _cpplint_state.quiet
+
+def _SetQuiet(quiet):
+ """Set the module's quiet status, and return previous setting."""
+ return _cpplint_state.SetQuiet(quiet)
+
def _VerboseLevel():
"""Returns the module's verbosity setting."""
@@ -946,7 +1605,10 @@ def Check(self, error, filename, linenum):
filename: The name of the current file.
linenum: The number of the line to check.
"""
- if Match(r'T(EST|est)', self.current_function):
+ if not self.in_a_function:
+ return
+
+ if re.match(r'T(EST|est)', self.current_function):
base_trigger = self._TEST_TRIGGER
else:
base_trigger = self._NORMAL_TRIGGER
@@ -959,9 +1621,8 @@ def Check(self, error, filename, linenum):
error_level = 5
error(filename, linenum, 'readability/fn_size', error_level,
'Small and focused functions are preferred:'
- ' %s has %d non-comment lines'
- ' (error triggered by exceeding %d lines).' % (
- self.current_function, self.lines_in_function, trigger))
+ f' {self.current_function} has {self.lines_in_function} non-comment lines'
+ f' (error triggered by exceeding {trigger} lines).')
def End(self):
"""Stop analyzing function body."""
@@ -988,12 +1649,12 @@ def FullName(self):
return os.path.abspath(self._filename).replace('\\', '/')
def RepositoryName(self):
- """FullName after removing the local path to the repository.
+ r"""FullName after removing the local path to the repository.
If we have a real absolute path name here we can try to do something smart:
detecting the root of the checkout and truncating /path/to/checkout from
the name so that we get header guards that don't include things like
- "C:\Documents and Settings\..." or "/home/username/..." in them and thus
+ "C:\\Documents and Settings\\..." or "/home/username/..." in them and thus
people on different computers who have checked the source out to different
locations won't see bogus errors.
"""
@@ -1002,6 +1663,20 @@ def RepositoryName(self):
if os.path.exists(fullname):
project_dir = os.path.dirname(fullname)
+ # If the user specified a repository path, it exists, and the file is
+ # contained in it, use the specified repository path
+ if _repository:
+ repo = FileInfo(_repository).FullName()
+ root_dir = project_dir
+ while os.path.exists(root_dir):
+ # allow case insensitive compare on Windows
+ if os.path.normcase(root_dir) == os.path.normcase(repo):
+ return os.path.relpath(fullname, root_dir).replace('\\', '/')
+ one_up_dir = os.path.dirname(root_dir)
+ if one_up_dir == root_dir:
+ break
+ root_dir = one_up_dir
+
if os.path.exists(os.path.join(project_dir, ".svn")):
# If there's a .svn file in the current directory, we recursively look
# up the directory tree for the top of the SVN checkout
@@ -1016,12 +1691,14 @@ def RepositoryName(self):
# Not SVN <= 1.6? Try to find a git, hg, or svn top level directory by
# searching up from the current path.
- root_dir = os.path.dirname(fullname)
- while (root_dir != os.path.dirname(root_dir) and
- not os.path.exists(os.path.join(root_dir, ".git")) and
- not os.path.exists(os.path.join(root_dir, ".hg")) and
- not os.path.exists(os.path.join(root_dir, ".svn"))):
- root_dir = os.path.dirname(root_dir)
+ root_dir = current_dir = os.path.dirname(fullname)
+ while current_dir != os.path.dirname(current_dir):
+ if (os.path.exists(os.path.join(current_dir, ".git")) or
+ os.path.exists(os.path.join(current_dir, ".hg")) or
+ os.path.exists(os.path.join(current_dir, ".svn"))):
+ root_dir = current_dir
+ break
+ current_dir = os.path.dirname(current_dir)
if (os.path.exists(os.path.join(root_dir, ".git")) or
os.path.exists(os.path.join(root_dir, ".hg")) or
@@ -1051,7 +1728,7 @@ def BaseName(self):
return self.Split()[1]
def Extension(self):
- """File extension - text following the final period."""
+ """File extension - text following the final period, includes that period."""
return self.Split()[2]
def NoExtension(self):
@@ -1060,10 +1737,10 @@ def NoExtension(self):
def IsSource(self):
"""File has a source file extension."""
- return self.Extension()[1:] in ('c', 'cc', 'cpp', 'cxx')
+ return _IsSourceExtension(self.Extension()[1:])
-def _ShouldPrintError(category, confidence, linenum):
+def _ShouldPrintError(category, confidence, filename, linenum):
"""If confidence >= verbose, category passes filter and is not suppressed."""
# There are three ways we might decide not to print an error message:
@@ -1077,11 +1754,16 @@ def _ShouldPrintError(category, confidence, linenum):
is_filtered = False
for one_filter in _Filters():
+ filter_cat, filter_file, filter_line = _ParseFilterSelector(one_filter[1:])
+ category_match = category.startswith(filter_cat)
+ file_match = filter_file == "" or filter_file == filename
+ line_match = filter_line == linenum or filter_line == -1
+
if one_filter.startswith('-'):
- if category.startswith(one_filter[1:]):
+ if category_match and file_match and line_match:
is_filtered = True
elif one_filter.startswith('+'):
- if category.startswith(one_filter[1:]):
+ if category_match and file_match and line_match:
is_filtered = False
else:
assert False # should have been checked for in SetFilter.
@@ -1098,9 +1780,9 @@ def Error(filename, linenum, category, confidence, message):
that is, how certain we are this is a legitimate style regression, and
not a misidentification or a use that's sometimes justified.
- False positives can be suppressed by the use of
- "cpplint(category)" comments on the offending line. These are
- parsed into _error_suppressions.
+ False positives can be suppressed by the use of "NOLINT(category)"
+ comments, NOLINTNEXTLINE or in blocks started by NOLINTBEGIN. These
+ are parsed into _error_suppressions.
Args:
filename: The name of the file containing the error.
@@ -1113,17 +1795,28 @@ def Error(filename, linenum, category, confidence, message):
and 1 meaning that it could be a legitimate construct.
message: The error message.
"""
- if _ShouldPrintError(category, confidence, linenum):
+ if _ShouldPrintError(category, confidence, filename, linenum):
_cpplint_state.IncrementErrorCount(category)
if _cpplint_state.output_format == 'vs7':
- sys.stderr.write('%s(%s): %s [%s] [%d]\n' % (
- filename, linenum, message, category, confidence))
+ _cpplint_state.PrintError(f'{filename}({linenum}): error cpplint:'
+ f' [{category}] {message} [{confidence}]\n')
elif _cpplint_state.output_format == 'eclipse':
- sys.stderr.write('%s:%s: warning: %s [%s] [%d]\n' % (
- filename, linenum, message, category, confidence))
+ sys.stderr.write(f'{filename}:{linenum}: warning:'
+ f' {message} [{category}] [{confidence}]\n')
+ elif _cpplint_state.output_format == 'junit':
+ _cpplint_state.AddJUnitFailure(filename, linenum, message, category, confidence)
+ elif _cpplint_state.output_format in ['sed', 'gsed']:
+ if message in _SED_FIXUPS:
+ sys.stdout.write(f"{_cpplint_state.output_format} -i"
+ f" '{linenum}{_SED_FIXUPS[message]}' {filename}"
+ f" # {message} [{category}] [{confidence}]\n")
+ else:
+ sys.stderr.write(f'# {filename}:{linenum}: '
+ f' "{message}" [{category}] [{confidence}]\n')
else:
- sys.stderr.write('%s:%s: %s [%s] [%d]\n' % (
- filename, linenum, message, category, confidence))
+ final_message = (f'{filename}:{linenum}: '
+ f' {message} [{category}] [{confidence}]\n')
+ sys.stderr.write(final_message)
# Matches standard C++ escape sequences per 2.13.2.3 of the C++ standard.
@@ -1193,7 +1886,7 @@ def CleanseRawStrings(raw_lines):
# Found the end of the string, match leading space for this
# line and resume copying the original lines, and also insert
# a "" on the last line.
- leading_space = Match(r'^(\s*)\S', line)
+ leading_space = re.match(r'^(\s*)\S', line)
line = leading_space.group(1) + '""' + line[end + len(delimiter):]
delimiter = None
else:
@@ -1206,8 +1899,18 @@ def CleanseRawStrings(raw_lines):
while delimiter is None:
# Look for beginning of a raw string.
# See 2.14.15 [lex.string] for syntax.
- matched = Match(r'^(.*)\b(?:R|u8R|uR|UR|LR)"([^\s\\()]*)\((.*)$', line)
- if matched:
+ #
+ # Once we have matched a raw string, we check the prefix of the
+ # line to make sure that the line is not part of a single line
+ # comment. It's done this way because we remove raw strings
+ # before removing comments as opposed to removing comments
+ # before removing raw strings. This is because there are some
+ # cpplint checks that requires the comments to be preserved, but
+ # we don't want to check comments that are inside raw strings.
+ matched = re.match(r'^(.*?)\b(?:R|u8R|uR|UR|LR)"([^\s\\()]*)\((.*)$', line)
+ if (matched and
+ not re.match(r'^([^\'"]|\'(\\.|[^\'])*\'|"(\\.|[^"])*")*//',
+ matched.group(1))):
delimiter = ')' + matched.group(2) + '"'
end = matched.group(3).find(delimiter)
@@ -1251,7 +1954,7 @@ def FindNextMultiLineCommentEnd(lines, lineix):
def RemoveMultiLineCommentsFromRange(lines, begin, end):
"""Clears a range of lines for multi-line comments."""
- # Having // dummy comments makes the lines non-empty, so we will not get
+ # Having // comments makes the lines non-empty, so we will not get
# unnecessary blank line warnings later in the code.
for i in range(begin, end):
lines[i] = '/**/'
@@ -1289,6 +1992,28 @@ def CleanseComments(line):
return _RE_PATTERN_CLEANSE_LINE_C_COMMENTS.sub('', line)
+def ReplaceAlternateTokens(line):
+ """Replace any alternate token by its original counterpart.
+
+ In order to comply with the google rule stating that unary operators should
+ never be followed by a space, an exception is made for the 'not' and 'compl'
+ alternate tokens. For these, any trailing space is removed during the
+ conversion.
+
+ Args:
+ line: The line being processed.
+
+ Returns:
+ The line with alternate tokens replaced.
+ """
+ for match in _ALT_TOKEN_REPLACEMENT_PATTERN.finditer(line):
+ token = _ALT_TOKEN_REPLACEMENT[match.group(2)]
+ tail = '' if match.group(2) in ['not', 'compl'] and match.group(3) == ' ' \
+ else r'\3'
+ line = re.sub(match.re, rf'\1{token}{tail}', line, count=1)
+ return line
+
+
class CleansedLines(object):
"""Holds 4 copies of all lines with different preprocessing applied to them.
@@ -1301,15 +2026,17 @@ class CleansedLines(object):
"""
def __init__(self, lines):
+ if '-readability/alt_tokens' in _cpplint_state.filters:
+ for i, line in enumerate(lines):
+ lines[i] = ReplaceAlternateTokens(line)
self.elided = []
self.lines = []
self.raw_lines = lines
self.num_lines = len(lines)
self.lines_without_raw_strings = CleanseRawStrings(lines)
- for linenum in range(len(self.lines_without_raw_strings)):
- self.lines.append(CleanseComments(
- self.lines_without_raw_strings[linenum]))
- elided = self._CollapseStrings(self.lines_without_raw_strings[linenum])
+ for line in self.lines_without_raw_strings:
+ self.lines.append(CleanseComments(line))
+ elided = self._CollapseStrings(line)
self.elided.append(CleanseComments(elided))
def NumLines(self):
@@ -1342,7 +2069,7 @@ def _CollapseStrings(elided):
collapsed = ''
while True:
# Find the first quote character
- match = Match(r'^([^\'"]*)([\'"])(.*)$', elided)
+ match = re.match(r'^([^\'"]*)([\'"])(.*)$', elided)
if not match:
collapsed += elided
break
@@ -1367,8 +2094,8 @@ def _CollapseStrings(elided):
# correctly as long as there are digits on both sides of the
# separator. So we are fine as long as we don't see something
# like "0.'3" (gcc 4.9.0 will not allow this literal).
- if Search(r'\b(?:0[bBxX]?|[1-9])[0-9a-fA-F]*$', head):
- match_literal = Match(r'^((?:\'?[0-9a-zA-Z_])*)(.*)$', "'" + tail)
+ if re.search(r'\b(?:0[bBxX]?|[1-9])[0-9a-fA-F]*$', head):
+ match_literal = re.match(r'^((?:\'?[0-9a-zA-Z_])*)(.*)$', "'" + tail)
collapsed += head + match_literal.group(1).replace("'", '')
elided = match_literal.group(2)
else:
@@ -1397,7 +2124,7 @@ def FindEndOfExpressionInLine(line, startpos, stack):
On finding an unclosed expression: (-1, None)
Otherwise: (-1, new stack at end of this line)
"""
- for i in xrange(startpos, len(line)):
+ for i in range(startpos, len(line)):
char = line[i]
if char in '([{':
# Found start of parenthesized expression, push to expression stack
@@ -1410,7 +2137,7 @@ def FindEndOfExpressionInLine(line, startpos, stack):
stack.pop()
if not stack:
return (-1, None)
- elif i > 0 and Search(r'\boperator\s*$', line[0:i]):
+ elif i > 0 and re.search(r'\boperator\s*$', line[0:i]):
# operator<, don't add to stack
continue
else:
@@ -1439,7 +2166,7 @@ def FindEndOfExpressionInLine(line, startpos, stack):
# Ignore "->" and operator functions
if (i > 0 and
- (line[i - 1] == '-' or Search(r'\boperator\s*$', line[0:i - 1]))):
+ (line[i - 1] == '-' or re.search(r'\boperator\s*$', line[0:i - 1]))):
continue
# Pop the stack if there is a matching '<'. Otherwise, ignore
@@ -1486,7 +2213,7 @@ def CloseExpression(clean_lines, linenum, pos):
"""
line = clean_lines.elided[linenum]
- if (line[pos] not in '({[<') or Match(r'<[<=]', line[pos:]):
+ if (line[pos] not in '({[<') or re.match(r'<[<=]', line[pos:]):
return (line, clean_lines.NumLines(), -1)
# Check first line
@@ -1534,8 +2261,8 @@ def FindStartOfExpressionInLine(line, endpos, stack):
# Ignore it if it's a "->" or ">=" or "operator>"
if (i > 0 and
(line[i - 1] == '-' or
- Match(r'\s>=\s', line[i - 1:]) or
- Search(r'\boperator\s*$', line[0:i]))):
+ re.match(r'\s>=\s', line[i - 1:]) or
+ re.search(r'\boperator\s*$', line[0:i]))):
i -= 1
else:
stack.append('>')
@@ -1625,8 +2352,8 @@ def CheckForCopyright(filename, lines, error):
"""Logs an error if no Copyright message appears at the top of the file."""
# We'll say it should occur by line 10. Don't forget there's a
- # dummy line at the front.
- for line in xrange(1, min(len(lines), 11)):
+ # placeholder line at the front.
+ for line in range(1, min(len(lines), 11)):
if re.search(r'Copyright', lines[line], re.I): break
else: # means no copyright line was found
error(filename, 0, 'legal/copyright', 5,
@@ -1643,12 +2370,36 @@ def GetIndentLevel(line):
Returns:
An integer count of leading spaces, possibly zero.
"""
- indent = Match(r'^( *)\S', line)
+ indent = re.match(r'^( *)\S', line)
if indent:
return len(indent.group(1))
else:
return 0
+def PathSplitToList(path):
+ """Returns the path split into a list by the separator.
+
+ Args:
+ path: An absolute or relative path (e.g. '/a/b/c/' or '../a')
+
+ Returns:
+ A list of path components (e.g. ['a', 'b', 'c]).
+ """
+ lst = []
+ while True:
+ (head, tail) = os.path.split(path)
+ if head == path: # absolute paths end
+ lst.append(head)
+ break
+ if tail == path: # relative paths end
+ lst.append(tail)
+ break
+
+ path = head
+ lst.append(tail)
+
+ lst.reverse()
+ return lst
def GetHeaderGuardCPPVariable(filename):
"""Returns the CPP variable that should be used as a header guard.
@@ -1668,11 +2419,62 @@ def GetHeaderGuardCPPVariable(filename):
filename = re.sub(r'/\.flymake/([^/]*)$', r'/\1', filename)
# Replace 'c++' with 'cpp'.
filename = filename.replace('C++', 'cpp').replace('c++', 'cpp')
-
+
fileinfo = FileInfo(filename)
file_path_from_root = fileinfo.RepositoryName()
- if _root:
- file_path_from_root = re.sub('^' + _root + os.sep, '', file_path_from_root)
+
+ def FixupPathFromRoot():
+ if _root_debug:
+ sys.stderr.write(f"\n_root fixup, _root = '{_root}',"
+ f" repository name = '{fileinfo.RepositoryName()}'\n")
+
+ # Process the file path with the --root flag if it was set.
+ if not _root:
+ if _root_debug:
+ sys.stderr.write("_root unspecified\n")
+ return file_path_from_root
+
+ def StripListPrefix(lst, prefix):
+ # f(['x', 'y'], ['w, z']) -> None (not a valid prefix)
+ if lst[:len(prefix)] != prefix:
+ return None
+ # f(['a, 'b', 'c', 'd'], ['a', 'b']) -> ['c', 'd']
+ return lst[(len(prefix)):]
+
+ # root behavior:
+ # --root=subdir , lstrips subdir from the header guard
+ maybe_path = StripListPrefix(PathSplitToList(file_path_from_root),
+ PathSplitToList(_root))
+
+ if _root_debug:
+ sys.stderr.write(("_root lstrip (maybe_path=%s, file_path_from_root=%s," +
+ " _root=%s)\n") % (maybe_path, file_path_from_root, _root))
+
+ if maybe_path:
+ return os.path.join(*maybe_path)
+
+ # --root=.. , will prepend the outer directory to the header guard
+ full_path = fileinfo.FullName()
+ # adapt slashes for windows
+ root_abspath = os.path.abspath(_root).replace('\\', '/')
+
+ maybe_path = StripListPrefix(PathSplitToList(full_path),
+ PathSplitToList(root_abspath))
+
+ if _root_debug:
+ sys.stderr.write(("_root prepend (maybe_path=%s, full_path=%s, " +
+ "root_abspath=%s)\n") % (maybe_path, full_path, root_abspath))
+
+ if maybe_path:
+ return os.path.join(*maybe_path)
+
+ if _root_debug:
+ sys.stderr.write(f"_root ignore, returning {file_path_from_root}\n")
+
+ # --root=FAKE_DIR is ignored
+ return file_path_from_root
+
+ file_path_from_root = FixupPathFromRoot()
return re.sub(r'[^a-zA-Z0-9]', '_', file_path_from_root).upper() + '_'
@@ -1696,7 +2498,12 @@ def CheckForHeaderGuard(filename, clean_lines, error):
# and not the general NOLINT or NOLINT(*) syntax.
raw_lines = clean_lines.lines_without_raw_strings
for i in raw_lines:
- if Search(r'//\s*NOLINT\(build/header_guard\)', i):
+ if re.search(r'//\s*NOLINT\(build/header_guard\)', i):
+ return
+
+ # Allow pragma once instead of header guards
+ for i in raw_lines:
+ if re.search(r'^\s*#pragma\s+once', i):
return
cppvar = GetHeaderGuardCPPVariable(filename)
@@ -1723,8 +2530,7 @@ def CheckForHeaderGuard(filename, clean_lines, error):
if not ifndef or not define or ifndef != define:
error(filename, 0, 'build/header_guard', 5,
- 'No #ifndef header guard found, suggested CPP variable is: %s' %
- cppvar)
+ f'No #ifndef header guard found, suggested CPP variable is: {cppvar}')
return
# The guard should be PATH_FILE_H_, but we also allow PATH_FILE_H__
@@ -1737,66 +2543,75 @@ def CheckForHeaderGuard(filename, clean_lines, error):
ParseNolintSuppressions(filename, raw_lines[ifndef_linenum], ifndef_linenum,
error)
error(filename, ifndef_linenum, 'build/header_guard', error_level,
- '#ifndef header guard has wrong style, please use: %s' % cppvar)
+ f'#ifndef header guard has wrong style, please use: {cppvar}')
# Check for "//" comments on endif line.
ParseNolintSuppressions(filename, raw_lines[endif_linenum], endif_linenum,
error)
- match = Match(r'#endif\s*//\s*' + cppvar + r'(_)?\b', endif)
+ match = re.match(r'#endif\s*//\s*' + cppvar + r'(_)?\b', endif)
if match:
if match.group(1) == '_':
# Issue low severity warning for deprecated double trailing underscore
error(filename, endif_linenum, 'build/header_guard', 0,
- '#endif line should be "#endif // %s"' % cppvar)
+ f'#endif line should be "#endif // {cppvar}"')
return
# Didn't find the corresponding "//" comment. If this file does not
# contain any "//" comments at all, it could be that the compiler
# only wants "/**/" comments, look for those instead.
no_single_line_comments = True
- for i in xrange(1, len(raw_lines) - 1):
+ for i in range(1, len(raw_lines) - 1):
line = raw_lines[i]
- if Match(r'^(?:(?:\'(?:\.|[^\'])*\')|(?:"(?:\.|[^"])*")|[^\'"])*//', line):
+ if re.match(r'^(?:(?:\'(?:\.|[^\'])*\')|(?:"(?:\.|[^"])*")|[^\'"])*//', line):
no_single_line_comments = False
break
if no_single_line_comments:
- match = Match(r'#endif\s*/\*\s*' + cppvar + r'(_)?\s*\*/', endif)
+ match = re.match(r'#endif\s*/\*\s*' + cppvar + r'(_)?\s*\*/', endif)
if match:
if match.group(1) == '_':
# Low severity warning for double trailing underscore
error(filename, endif_linenum, 'build/header_guard', 0,
- '#endif line should be "#endif /* %s */"' % cppvar)
+ f'#endif line should be "#endif /* {cppvar} */"')
return
# Didn't find anything
error(filename, endif_linenum, 'build/header_guard', 5,
- '#endif line should be "#endif // %s"' % cppvar)
+ f'#endif line should be "#endif // {cppvar}"')
def CheckHeaderFileIncluded(filename, include_state, error):
- """Logs an error if a .cc file does not include its header."""
+ """Logs an error if a source file does not include its header."""
# Do not check test files
- if filename.endswith('_test.cc') or filename.endswith('_unittest.cc'):
- return
-
fileinfo = FileInfo(filename)
- headerfile = filename[0:len(filename) - 2] + 'h'
- if not os.path.exists(headerfile):
+ if re.search(_TEST_FILE_SUFFIX, fileinfo.BaseName()):
return
- headername = FileInfo(headerfile).RepositoryName()
- first_include = 0
- for section_list in include_state.include_list:
- for f in section_list:
- if headername in f[0] or f[0] in headername:
- return
- if not first_include:
- first_include = f[1]
- error(filename, first_include, 'build/include', 5,
- '%s should include its header file %s' % (fileinfo.RepositoryName(),
- headername))
+ first_include = message = None
+ basefilename = filename[0:len(filename) - len(fileinfo.Extension())]
+ for ext in GetHeaderExtensions():
+ headerfile = basefilename + '.' + ext
+ if not os.path.exists(headerfile):
+ continue
+ headername = FileInfo(headerfile).RepositoryName()
+ include_uses_unix_dir_aliases = False
+ for section_list in include_state.include_list:
+ for f in section_list:
+ include_text = f[0]
+ if "./" in include_text:
+ include_uses_unix_dir_aliases = True
+ if headername in include_text or include_text in headername:
+ return
+ if not first_include:
+ first_include = f[1]
+
+ message = f'{fileinfo.RepositoryName()} should include its header file {headername}'
+ if include_uses_unix_dir_aliases:
+ message += ". Relative paths like . and .. are not allowed."
+
+ if message:
+ error(filename, first_include, 'build/include', 5, message)
def CheckForBadCharacters(filename, lines, error):
@@ -1817,7 +2632,7 @@ def CheckForBadCharacters(filename, lines, error):
error: The function to call with any errors found.
"""
for linenum, line in enumerate(lines):
- if u'\ufffd' in line:
+ if '\ufffd' in line:
error(filename, linenum, 'readability/utf8', 5,
'Line contains invalid UTF-8 (or Unicode replacement character).')
if '\0' in line:
@@ -1929,7 +2744,7 @@ def CheckPosixThreading(filename, clean_lines, linenum, error):
for single_thread_func, multithread_safe_func, pattern in _THREADING_LIST:
# Additional pattern matching check to confirm that this is the
# function we are looking for
- if Search(pattern, line):
+ if re.search(pattern, line):
error(filename, linenum, 'runtime/threadsafe_fn', 2,
'Consider using ' + multithread_safe_func +
'...) instead of ' + single_thread_func +
@@ -1949,7 +2764,7 @@ def CheckVlogArguments(filename, clean_lines, linenum, error):
error: The function to call with any errors found.
"""
line = clean_lines.elided[linenum]
- if Search(r'\bVLOG\((INFO|ERROR|WARNING|DFATAL|FATAL)\)', line):
+ if re.search(r'\bVLOG\((INFO|ERROR|WARNING|DFATAL|FATAL)\)', line):
error(filename, linenum, 'runtime/vlog', 5,
'VLOG() should be used with numeric verbosity level. '
'Use LOG() if you want symbolic severity levels.')
@@ -1983,23 +2798,24 @@ def CheckInvalidIncrement(filename, clean_lines, linenum, error):
def IsMacroDefinition(clean_lines, linenum):
- if Search(r'^#define', clean_lines[linenum]):
+ if re.search(r'^#define', clean_lines[linenum]):
return True
- if linenum > 0 and Search(r'\\$', clean_lines[linenum - 1]):
+ if linenum > 0 and re.search(r'\\$', clean_lines[linenum - 1]):
return True
return False
def IsForwardClassDeclaration(clean_lines, linenum):
- return Match(r'^\s*(\btemplate\b)*.*class\s+\w+;\s*$', clean_lines[linenum])
+ return re.match(r'^\s*(\btemplate\b)*.*class\s+\w+;\s*$', clean_lines[linenum])
class _BlockInfo(object):
"""Stores information about a generic block of code."""
- def __init__(self, seen_open_brace):
+ def __init__(self, linenum, seen_open_brace):
+ self.starting_linenum = linenum
self.seen_open_brace = seen_open_brace
self.open_parentheses = 0
self.inline_asm = _NO_ASM
@@ -2048,17 +2864,16 @@ def IsBlockInfo(self):
class _ExternCInfo(_BlockInfo):
"""Stores information about an 'extern "C"' block."""
- def __init__(self):
- _BlockInfo.__init__(self, True)
+ def __init__(self, linenum):
+ _BlockInfo.__init__(self, linenum, True)
class _ClassInfo(_BlockInfo):
"""Stores information about a class."""
def __init__(self, name, class_or_struct, clean_lines, linenum):
- _BlockInfo.__init__(self, False)
+ _BlockInfo.__init__(self, linenum, False)
self.name = name
- self.starting_linenum = linenum
self.is_derived = False
self.check_namespace_indentation = True
if class_or_struct == 'struct':
@@ -2088,15 +2903,15 @@ def __init__(self, name, class_or_struct, clean_lines, linenum):
def CheckBegin(self, filename, clean_lines, linenum, error):
# Look for a bare ':'
- if Search('(^|[^:]):($|[^:])', clean_lines.elided[linenum]):
+ if re.search('(^|[^:]):($|[^:])', clean_lines.elided[linenum]):
self.is_derived = True
def CheckEnd(self, filename, clean_lines, linenum, error):
# If there is a DISALLOW macro, it should appear near the end of
# the class.
seen_last_thing_in_class = False
- for i in xrange(linenum - 1, self.starting_linenum, -1):
- match = Search(
+ for i in range(linenum - 1, self.starting_linenum, -1):
+ match = re.search(
r'\b(DISALLOW_COPY_AND_ASSIGN|DISALLOW_IMPLICIT_CONSTRUCTORS)\(' +
self.name + r'\)',
clean_lines.elided[i])
@@ -2106,29 +2921,28 @@ def CheckEnd(self, filename, clean_lines, linenum, error):
match.group(1) + ' should be the last thing in the class')
break
- if not Match(r'^\s*$', clean_lines.elided[i]):
+ if not re.match(r'^\s*$', clean_lines.elided[i]):
seen_last_thing_in_class = True
# Check that closing brace is aligned with beginning of the class.
# Only do this if the closing brace is indented by only whitespaces.
# This means we will not check single-line class definitions.
- indent = Match(r'^( *)\}', clean_lines.elided[linenum])
+ indent = re.match(r'^( *)\}', clean_lines.elided[linenum])
if indent and len(indent.group(1)) != self.class_indent:
if self.is_struct:
parent = 'struct ' + self.name
else:
parent = 'class ' + self.name
error(filename, linenum, 'whitespace/indent', 3,
- 'Closing brace should be aligned with beginning of %s' % parent)
+ f'Closing brace should be aligned with beginning of {parent}')
class _NamespaceInfo(_BlockInfo):
"""Stores information about a namespace."""
def __init__(self, name, linenum):
- _BlockInfo.__init__(self, False)
+ _BlockInfo.__init__(self, linenum, False)
self.name = name or ''
- self.starting_linenum = linenum
self.check_namespace_indentation = True
def CheckEnd(self, filename, clean_lines, linenum, error):
@@ -2147,7 +2961,7 @@ def CheckEnd(self, filename, clean_lines, linenum, error):
# deciding what these nontrivial things are, so this check is
# triggered by namespace size only, which works most of the time.
if (linenum - self.starting_linenum < 10
- and not Match(r'};*\s*(//|/\*).*\bnamespace\b', line)):
+ and not re.match(r'^\s*};*\s*(//|/\*).*\bnamespace\b', line)):
return
# Look for matching comment at end of namespace.
@@ -2164,18 +2978,17 @@ def CheckEnd(self, filename, clean_lines, linenum, error):
# expected namespace.
if self.name:
# Named namespace
- if not Match((r'};*\s*(//|/\*).*\bnamespace\s+' + re.escape(self.name) +
- r'[\*/\.\\\s]*$'),
+ if not re.match((r'^\s*};*\s*(//|/\*).*\bnamespace\s+' +
+ re.escape(self.name) + r'[\*/\.\\\s]*$'),
line):
error(filename, linenum, 'readability/namespace', 5,
- 'Namespace should be terminated with "// namespace %s"' %
- self.name)
+ f'Namespace should be terminated with "// namespace {self.name}"')
else:
# Anonymous namespace
- if not Match(r'};*\s*(//|/\*).*\bnamespace[\*/\.\\\s]*$', line):
+ if not re.match(r'^\s*};*\s*(//|/\*).*\bnamespace[\*/\.\\\s]*$', line):
# If "// namespace anonymous" or "// anonymous namespace (more text)",
# mention "// anonymous namespace" as an acceptable form
- if Match(r'}.*\b(namespace anonymous|anonymous namespace)\b', line):
+ if re.match(r'^\s*}.*\b(namespace anonymous|anonymous namespace)\b', line):
error(filename, linenum, 'readability/namespace', 5,
'Anonymous namespace should be terminated with "// namespace"'
' or "// anonymous namespace"')
@@ -2278,7 +3091,7 @@ def InTemplateArgumentList(self, clean_lines, linenum, pos):
while linenum < clean_lines.NumLines():
# Find the earliest character that might indicate a template argument
line = clean_lines.elided[linenum]
- match = Match(r'^[^{};=\[\]\.<>]*(.)', line[pos:])
+ match = re.match(r'^[^{};=\[\]\.<>]*(.)', line[pos:])
if not match:
linenum += 1
pos = 0
@@ -2338,11 +3151,11 @@ def UpdatePreprocessor(self, line):
Args:
line: current line to check.
"""
- if Match(r'^\s*#\s*(if|ifdef|ifndef)\b', line):
+ if re.match(r'^\s*#\s*(if|ifdef|ifndef)\b', line):
# Beginning of #if block, save the nesting stack here. The saved
# stack will allow us to restore the parsing state in the #else case.
self.pp_stack.append(_PreprocessorInfo(copy.deepcopy(self.stack)))
- elif Match(r'^\s*#\s*(else|elif)\b', line):
+ elif re.match(r'^\s*#\s*(else|elif)\b', line):
# Beginning of #else block
if self.pp_stack:
if not self.pp_stack[-1].seen_else:
@@ -2357,7 +3170,7 @@ def UpdatePreprocessor(self, line):
else:
# TODO(unknown): unexpected #else, issue warning?
pass
- elif Match(r'^\s*#\s*endif\b', line):
+ elif re.match(r'^\s*#\s*endif\b', line):
# End of #if or #else blocks.
if self.pp_stack:
# If we saw an #else, we will need to restore the nesting
@@ -2429,7 +3242,7 @@ def Update(self, filename, clean_lines, linenum, error):
# declarations even if it weren't followed by a whitespace, this
# is so that we don't confuse our namespace checker. The
# missing spaces will be flagged by CheckSpacing.
- namespace_decl_match = Match(r'^\s*namespace\b\s*([:\w]+)?(.*)$', line)
+ namespace_decl_match = re.match(r'^\s*namespace\b\s*([:\w]+)?(.*)$', line)
if not namespace_decl_match:
break
@@ -2446,9 +3259,9 @@ def Update(self, filename, clean_lines, linenum, error):
# such as in:
# class LOCKABLE API Object {
# };
- class_decl_match = Match(
- r'^(\s*(?:template\s*<[\w\s<>,:]*>\s*)?'
- r'(class|struct)\s+(?:[A-Z_]+\s+)*(\w+(?:::\w+)*))'
+ class_decl_match = re.match(
+ r'^(\s*(?:template\s*<[\w\s<>,:=]*>\s*)?'
+ r'(class|struct)\s+(?:[a-zA-Z0-9_]+\s+)*(\w+(?:::\w+)*))'
r'(.*)$', line)
if (class_decl_match and
(not self.stack or self.stack[-1].open_parentheses == 0)):
@@ -2476,7 +3289,7 @@ def Update(self, filename, clean_lines, linenum, error):
# Update access control if we are inside a class/struct
if self.stack and isinstance(self.stack[-1], _ClassInfo):
classinfo = self.stack[-1]
- access_match = Match(
+ access_match = re.match(
r'^(.*)\b(public|private|protected|signals)(\s+(?:slots\s*)?)?'
r':(?:[^:]|$)',
line)
@@ -2487,7 +3300,7 @@ def Update(self, filename, clean_lines, linenum, error):
# check if the keywords are not preceded by whitespaces.
indent = access_match.group(1)
if (len(indent) != classinfo.class_indent + 1 and
- Match(r'^\s*$', indent)):
+ re.match(r'^\s*$', indent)):
if classinfo.is_struct:
parent = 'struct ' + classinfo.name
else:
@@ -2496,13 +3309,13 @@ def Update(self, filename, clean_lines, linenum, error):
if access_match.group(3):
slots = access_match.group(3)
error(filename, linenum, 'whitespace/indent', 3,
- '%s%s: should be indented +1 space inside %s' % (
- access_match.group(2), slots, parent))
+ f'{access_match.group(2)}{slots}:'
+ f' should be indented +1 space inside {parent}')
# Consume braces or semicolons from what's left of the line
while True:
# Match first brace, semicolon, or closed parenthesis.
- matched = Match(r'^[^{;)}]*([{;)}])(.*)$', line)
+ matched = re.match(r'^[^{;)}]*([{;)}])(.*)$', line)
if not matched:
break
@@ -2513,10 +3326,10 @@ def Update(self, filename, clean_lines, linenum, error):
# stack otherwise.
if not self.SeenOpenBrace():
self.stack[-1].seen_open_brace = True
- elif Match(r'^extern\s*"[^"]*"\s*\{', line):
- self.stack.append(_ExternCInfo())
+ elif re.match(r'^extern\s*"[^"]*"\s*\{', line):
+ self.stack.append(_ExternCInfo(linenum))
else:
- self.stack.append(_BlockInfo(True))
+ self.stack.append(_BlockInfo(linenum, True))
if _MATCH_ASM.match(line):
self.stack[-1].inline_asm = _BLOCK_ASM
@@ -2550,28 +3363,6 @@ def InnermostClass(self):
return classinfo
return None
- def CheckCompletedBlocks(self, filename, error):
- """Checks that all classes and namespaces have been completely parsed.
-
- Call this when all lines in a file have been processed.
- Args:
- filename: The name of the current file.
- error: The function to call with any errors found.
- """
- # Note: This test can result in false positives if #ifdef constructs
- # get in the way of brace matching. See the testBuildClass test in
- # cpplint_unittest.py for an example of this.
- for obj in self.stack:
- if isinstance(obj, _ClassInfo):
- error(filename, obj.starting_linenum, 'build/class', 5,
- 'Failed to find complete declaration of class %s' %
- obj.name)
- elif isinstance(obj, _NamespaceInfo):
- error(filename, obj.starting_linenum, 'build/namespaces', 5,
- 'Failed to find complete declaration of namespace %s' %
- obj.name)
-
-
def CheckForNonStandardConstructs(filename, clean_lines, linenum,
nesting_state, error):
r"""Logs an error if we see certain non-ANSI constructs ignored by gcc-2.
@@ -2604,46 +3395,47 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum,
# Remove comments from the line, but leave in strings for now.
line = clean_lines.lines[linenum]
- if Search(r'printf\s*\(.*".*%[-+ ]?\d*q', line):
+ if re.search(r'printf\s*\(.*".*%[-+ ]?\d*q', line):
error(filename, linenum, 'runtime/printf_format', 3,
'%q in format strings is deprecated. Use %ll instead.')
- if Search(r'printf\s*\(.*".*%\d+\$', line):
+ if re.search(r'printf\s*\(.*".*%\d+\$', line):
error(filename, linenum, 'runtime/printf_format', 2,
'%N$ formats are unconventional. Try rewriting to avoid them.')
# Remove escaped backslashes before looking for undefined escapes.
line = line.replace('\\\\', '')
- if Search(r'("|\').*\\(%|\[|\(|{)', line):
+ if re.search(r'("|\').*\\(%|\[|\(|{)', line):
error(filename, linenum, 'build/printf_format', 3,
'%, [, (, and { are undefined character escapes. Unescape them.')
# For the rest, work with both comments and strings removed.
line = clean_lines.elided[linenum]
- if Search(r'\b(const|volatile|void|char|short|int|long'
+ if re.search(r'\b(const|volatile|void|char|short|int|long'
r'|float|double|signed|unsigned'
r'|schar|u?int8|u?int16|u?int32|u?int64)'
r'\s+(register|static|extern|typedef)\b',
line):
error(filename, linenum, 'build/storage_class', 5,
- 'Storage class (static, extern, typedef, etc) should be first.')
+ 'Storage-class specifier (static, extern, typedef, etc) should be '
+ 'at the beginning of the declaration.')
- if Match(r'\s*#\s*endif\s*[^/\s]+', line):
+ if re.match(r'\s*#\s*endif\s*[^/\s]+', line):
error(filename, linenum, 'build/endif_comment', 5,
'Uncommented text after #endif is non-standard. Use a comment.')
- if Match(r'\s*class\s+(\w+\s*::\s*)+\w+\s*;', line):
+ if re.match(r'\s*class\s+(\w+\s*::\s*)+\w+\s*;', line):
error(filename, linenum, 'build/forward_decl', 5,
'Inner-style forward declarations are invalid. Remove this line.')
- if Search(r'(\w+|[+-]?\d+(\.\d*)?)\s*(<|>)\?=?\s*(\w+|[+-]?\d+)(\.\d*)?',
+ if re.search(r'(\w+|[+-]?\d+(\.\d*)?)\s*(<|>)\?=?\s*(\w+|[+-]?\d+)(\.\d*)?',
line):
error(filename, linenum, 'build/deprecated', 3,
'>? and (max and min) operators are non-standard and deprecated.')
- if Search(r'^\s*const\s*string\s*&\s*\w+\s*;', line):
+ if re.search(r'^\s*const\s*string\s*&\s*\w+\s*;', line):
# TODO(unknown): Could it be expanded safely to arbitrary references,
# without triggering too many false positives? The first
# attempt triggered 5 warnings for mostly benign code in the regtest, hence
@@ -2667,14 +3459,11 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum,
base_classname = classinfo.name.split('::')[-1]
# Look for single-argument constructors that aren't marked explicit.
- # Technically a valid construct, but against style. Also look for
- # non-single-argument constructors which are also technically valid, but
- # strongly suggest something is wrong.
- explicit_constructor_match = Match(
- r'\s+(?:inline\s+)?(explicit\s+)?(?:inline\s+)?%s\s*'
- r'\(((?:[^()]|\([^()]*\))*)\)'
- % re.escape(base_classname),
- line)
+ # Technically a valid construct, but against style.
+ explicit_constructor_match = re.match(
+ r'\s+(?:(?:inline|constexpr)\s+)*(explicit\s+)?'
+ rf'(?:(?:inline|constexpr)\s+)*{re.escape(base_classname)}\s*'
+ r'\(((?:[^()]|\([^()]*\))*)\)', line)
if explicit_constructor_match:
is_marked_explicit = explicit_constructor_match.group(1)
@@ -2696,6 +3485,7 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum,
constructor_args[i] = constructor_arg
i += 1
+ variadic_args = [arg for arg in constructor_args if '&&...' in arg]
defaulted_args = [arg for arg in constructor_args if '=' in arg]
noarg_constructor = (not constructor_args or # empty arg list
# 'void' arg specifier
@@ -2706,34 +3496,31 @@ def CheckForNonStandardConstructs(filename, clean_lines, linenum,
# all but at most one arg defaulted
(len(constructor_args) >= 1 and
not noarg_constructor and
- len(defaulted_args) >= len(constructor_args) - 1))
+ len(defaulted_args) >= len(constructor_args) - 1) or
+ # variadic arguments with zero or one argument
+ (len(constructor_args) <= 2 and
+ len(variadic_args) >= 1))
initializer_list_constructor = bool(
onearg_constructor and
- Search(r'\bstd\s*::\s*initializer_list\b', constructor_args[0]))
+ re.search(r'\bstd\s*::\s*initializer_list\b', constructor_args[0]))
copy_constructor = bool(
onearg_constructor and
- Match(r'(const\s+)?%s(\s*<[^>]*>)?(\s+const)?\s*(?:<\w+>\s*)?&'
- % re.escape(base_classname), constructor_args[0].strip()))
+ re.match(r'((const\s+(volatile\s+)?)?|(volatile\s+(const\s+)?))?'
+ rf'{re.escape(base_classname)}(\s*<[^>]*>)?(\s+const)?\s*(?:<\w+>\s*)?&',
+ constructor_args[0].strip())
+ )
if (not is_marked_explicit and
onearg_constructor and
not initializer_list_constructor and
not copy_constructor):
- if defaulted_args:
- error(filename, linenum, 'runtime/explicit', 5,
+ if defaulted_args or variadic_args:
+ error(filename, linenum, 'runtime/explicit', 4,
'Constructors callable with one argument '
'should be marked explicit.')
else:
- error(filename, linenum, 'runtime/explicit', 5,
+ error(filename, linenum, 'runtime/explicit', 4,
'Single-parameter constructors should be marked explicit.')
- elif is_marked_explicit and not onearg_constructor:
- if noarg_constructor:
- error(filename, linenum, 'runtime/explicit', 5,
- 'Zero-parameter constructors should not be marked explicit.')
- else:
- error(filename, linenum, 'runtime/explicit', 0,
- 'Constructors that require multiple arguments '
- 'should not be marked explicit.')
def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error):
@@ -2756,7 +3543,7 @@ def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error):
r'\bfor\s*\((.*)\)\s*{',
r'\bwhile\s*\((.*)\)\s*[{;]',
r'\bswitch\s*\((.*)\)\s*{'):
- match = Search(pattern, line)
+ match = re.search(pattern, line)
if match:
fncall = match.group(1) # look inside the parens for function calls
break
@@ -2775,25 +3562,26 @@ def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error):
# Note that we assume the contents of [] to be short enough that
# they'll never need to wrap.
if ( # Ignore control structures.
- not Search(r'\b(if|for|while|switch|return|new|delete|catch|sizeof)\b',
+ not re.search(r'\b(if|elif|for|while|switch|return|new|delete|catch|sizeof)\b',
fncall) and
# Ignore pointers/references to functions.
- not Search(r' \([^)]+\)\([^)]*(\)|,$)', fncall) and
+ not re.search(r' \([^)]+\)\([^)]*(\)|,$)', fncall) and
# Ignore pointers/references to arrays.
- not Search(r' \([^)]+\)\[[^\]]+\]', fncall)):
- if Search(r'\w\s*\(\s(?!\s*\\$)', fncall): # a ( used for a fn call
+ not re.search(r' \([^)]+\)\[[^\]]+\]', fncall)):
+ if re.search(r'\w\s*\(\s(?!\s*\\$)', fncall): # a ( used for a fn call
error(filename, linenum, 'whitespace/parens', 4,
'Extra space after ( in function call')
- elif Search(r'\(\s+(?!(\s*\\)|\()', fncall):
+ elif re.search(r'\(\s+(?!(\s*\\)|\()', fncall):
error(filename, linenum, 'whitespace/parens', 2,
'Extra space after (')
- if (Search(r'\w\s+\(', fncall) and
- not Search(r'#\s*define|typedef|using\s+\w+\s*=', fncall) and
- not Search(r'\w\s+\((\w+::)*\*\w+\)\(', fncall) and
- not Search(r'\bcase\s+\(', fncall)):
+ if (re.search(r'\w\s+\(', fncall) and
+ not re.search(r'_{0,2}asm_{0,2}\s+_{0,2}volatile_{0,2}\s+\(', fncall) and
+ not re.search(r'#\s*define|typedef|using\s+\w+\s*=', fncall) and
+ not re.search(r'\w\s+\((\w+::)*\*\w+\)\(', fncall) and
+ not re.search(r'\bcase\s+\(', fncall)):
# TODO(unknown): Space after an operator function seem to be a common
# error, silence those for now by restricting them to highest verbosity.
- if Search(r'\boperator_*\b', line):
+ if re.search(r'\boperator_*\b', line):
error(filename, linenum, 'whitespace/parens', 0,
'Extra space before ( in function call')
else:
@@ -2801,10 +3589,10 @@ def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error):
'Extra space before ( in function call')
# If the ) is followed only by a newline or a { + newline, assume it's
# part of a control statement (if/while/etc), and don't complain
- if Search(r'[^)]\s+\)\s*[^{\s]', fncall):
+ if re.search(r'[^)]\s+\)\s*[^{\s]', fncall):
# If the closing parenthesis is preceded by only whitespaces,
# try to give a more descriptive error message.
- if Search(r'^\s+\)', fncall):
+ if re.search(r'^\s+\)', fncall):
error(filename, linenum, 'whitespace/parens', 2,
'Closing ) should be moved to the previous line')
else:
@@ -2830,10 +3618,10 @@ def IsBlankLine(line):
def CheckForNamespaceIndentation(filename, nesting_state, clean_lines, line,
error):
is_namespace_indent_item = (
- len(nesting_state.stack) > 1 and
- nesting_state.stack[-1].check_namespace_indentation and
- isinstance(nesting_state.previous_stack_top, _NamespaceInfo) and
- nesting_state.previous_stack_top == nesting_state.stack[-2])
+ len(nesting_state.stack) >= 1 and
+ (isinstance(nesting_state.stack[-1], _NamespaceInfo) or
+ (isinstance(nesting_state.previous_stack_top, _NamespaceInfo)))
+ )
if ShouldCheckNamespaceIndentation(nesting_state, is_namespace_indent_item,
clean_lines.elided, line):
@@ -2846,7 +3634,7 @@ def CheckForFunctionLengths(filename, clean_lines, linenum,
"""Reports for long function bodies.
For an overview why this is done, see:
- http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Write_Short_Functions
+ https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Write_Short_Functions
Uses a simplistic algorithm assuming other style guidelines
(especially spacing) are followed.
@@ -2870,28 +3658,28 @@ def CheckForFunctionLengths(filename, clean_lines, linenum,
starting_func = False
regexp = r'(\w(\w|::|\*|\&|\s)*)\(' # decls * & space::name( ...
- match_result = Match(regexp, line)
+ match_result = re.match(regexp, line)
if match_result:
# If the name is all caps and underscores, figure it's a macro and
# ignore it, unless it's TEST or TEST_F.
function_name = match_result.group(1).split()[-1]
if function_name == 'TEST' or function_name == 'TEST_F' or (
- not Match(r'[A-Z_]+$', function_name)):
+ not re.match(r'[A-Z_]+$', function_name)):
starting_func = True
if starting_func:
body_found = False
- for start_linenum in xrange(linenum, clean_lines.NumLines()):
+ for start_linenum in range(linenum, clean_lines.NumLines()):
start_line = lines[start_linenum]
joined_line += ' ' + start_line.lstrip()
- if Search(r'(;|})', start_line): # Declarations and trivial functions
+ if re.search(r'(;|})', start_line): # Declarations and trivial functions
body_found = True
break # ... ignore
- elif Search(r'{', start_line):
+ if re.search(r'{', start_line):
body_found = True
- function = Search(r'((\w|:)*)\(', line).group(1)
- if Match(r'TEST', function): # Handle TEST... macros
- parameter_regexp = Search(r'(\(.*\))', joined_line)
+ function = re.search(r'((\w|:)*)\(', line).group(1)
+ if re.match(r'TEST', function): # Handle TEST... macros
+ parameter_regexp = re.search(r'(\(.*\))', joined_line)
if parameter_regexp: # Ignore bad syntax
function += parameter_regexp.group(1)
else:
@@ -2902,10 +3690,10 @@ def CheckForFunctionLengths(filename, clean_lines, linenum,
# No body for the function (or evidence of a non-function) was found.
error(filename, linenum, 'readability/fn_size', 5,
'Lint failed to find start of function body.')
- elif Match(r'^\}\s*$', line): # function end
+ elif re.match(r'^\}\s*$', line): # function end
function_state.Check(error, filename, linenum)
function_state.End()
- elif not Match(r'^\s*$', line):
+ elif not re.match(r'^\s*$', line):
function_state.Count() # Count non-blank/non-comment lines.
@@ -2925,11 +3713,9 @@ def CheckComment(line, filename, linenum, next_line_start, error):
commentpos = line.find('//')
if commentpos != -1:
# Check if the // may be in quotes. If so, ignore it
- # Comparisons made explicit for clarity -- pylint: disable=g-explicit-bool-comparison
- if (line.count('"', 0, commentpos) -
- line.count('\\"', 0, commentpos)) % 2 == 0: # not in quotes
+ if re.sub(r'\\.', '', line[0:commentpos]).count('"') % 2 == 0:
# Allow one space for new scopes, two spaces otherwise:
- if (not (Match(r'^.*{ *//', line) and next_line_start == commentpos) and
+ if (not (re.match(r'^.*{ *//', line) and next_line_start == commentpos) and
((commentpos >= 1 and
line[commentpos-1] not in string.whitespace) or
(commentpos >= 2 and
@@ -2954,7 +3740,8 @@ def CheckComment(line, filename, linenum, next_line_start, error):
'"// TODO(my_username): Stuff."')
middle_whitespace = match.group(3)
- # Comparisons made explicit for correctness -- pylint: disable=g-explicit-bool-comparison
+ # Comparisons made explicit for correctness
+ # -- pylint: disable=g-explicit-bool-comparison
if middle_whitespace != ' ' and middle_whitespace != '':
error(filename, linenum, 'whitespace/todo', 2,
'TODO(my_username) should be followed by a space')
@@ -2962,42 +3749,12 @@ def CheckComment(line, filename, linenum, next_line_start, error):
# If the comment contains an alphanumeric character, there
# should be a space somewhere between it and the // unless
# it's a /// or //! Doxygen comment.
- if (Match(r'//[^ ]*\w', comment) and
- not Match(r'(///|//\!)(\s+|$)', comment)):
+ if (re.match(r'//[^ ]*\w', comment) and
+ not re.match(r'(///|//\!)(\s+|$)', comment)):
error(filename, linenum, 'whitespace/comments', 4,
'Should have a space between // and comment')
-def CheckAccess(filename, clean_lines, linenum, nesting_state, error):
- """Checks for improper use of DISALLOW* macros.
-
- Args:
- filename: The name of the current file.
- clean_lines: A CleansedLines instance containing the file.
- linenum: The number of the line to check.
- nesting_state: A NestingState instance which maintains information about
- the current stack of nested blocks being parsed.
- error: The function to call with any errors found.
- """
- line = clean_lines.elided[linenum] # get rid of comments and strings
-
- matched = Match((r'\s*(DISALLOW_COPY_AND_ASSIGN|'
- r'DISALLOW_IMPLICIT_CONSTRUCTORS)'), line)
- if not matched:
- return
- if nesting_state.stack and isinstance(nesting_state.stack[-1], _ClassInfo):
- if nesting_state.stack[-1].access != 'private':
- error(filename, linenum, 'readability/constructors', 3,
- '%s must be in the private: section' % matched.group(1))
-
- else:
- # Found DISALLOW* macro outside a class declaration, or perhaps it
- # was used inside a function when it should have been part of the
- # class declaration. We could issue a warning here, but it
- # probably resulted in a compiler error already.
- pass
-
-
def CheckSpacing(filename, clean_lines, linenum, nesting_state, error):
"""Checks for the correctness of various spacing issues in the code.
@@ -3056,12 +3813,12 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error):
# the previous line is indented 6 spaces, which may happen when the
# initializers of a constructor do not fit into a 80 column line.
exception = False
- if Match(r' {6}\w', prev_line): # Initializer list?
+ if re.match(r' {6}\w', prev_line): # Initializer list?
# We are looking for the opening column of initializer list, which
# should be indented 4 spaces to cause 6 space indentation afterwards.
search_position = linenum-2
while (search_position >= 0
- and Match(r' {6}\w', elided[search_position])):
+ and re.match(r' {6}\w', elided[search_position])):
search_position -= 1
exception = (search_position >= 0
and elided[search_position][:5] == ' :')
@@ -3072,9 +3829,9 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error):
# or colon (for initializer lists) we assume that it is the last line of
# a function header. If we have a colon indented 4 spaces, it is an
# initializer list.
- exception = (Match(r' {4}\w[^\(]*\)\s*(const\s*)?(\{\s*$|:)',
+ exception = (re.match(r' {4}\w[^\(]*\)\s*(const\s*)?(\{\s*$|:)',
prev_line)
- or Match(r' {4}:', prev_line))
+ or re.match(r' {4}:', prev_line))
if not exception:
error(filename, linenum, 'whitespace/blank_line', 2,
@@ -3091,16 +3848,16 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error):
if linenum + 1 < clean_lines.NumLines():
next_line = raw[linenum + 1]
if (next_line
- and Match(r'\s*}', next_line)
+ and re.match(r'\s*}', next_line)
and next_line.find('} else ') == -1):
error(filename, linenum, 'whitespace/blank_line', 3,
'Redundant blank line at the end of a code block '
'should be deleted.')
- matched = Match(r'\s*(public|protected|private):', prev_line)
+ matched = re.match(r'\s*(public|protected|private):', prev_line)
if matched:
error(filename, linenum, 'whitespace/blank_line', 3,
- 'Do not leave a blank line after "%s:"' % matched.group(1))
+ f'Do not leave a blank line after "{matched.group(1)}:"')
# Next, check comments
next_line_start = 0
@@ -3112,16 +3869,17 @@ def CheckSpacing(filename, clean_lines, linenum, nesting_state, error):
# get rid of comments and strings
line = clean_lines.elided[linenum]
- # You shouldn't have spaces before your brackets, except maybe after
- # 'delete []' or 'return []() {};'
- if Search(r'\w\s+\[', line) and not Search(r'(?:delete|return)\s+\[', line):
+ # You shouldn't have spaces before your brackets, except for C++11 attributes
+ # or maybe after 'delete []', 'return []() {};', or 'auto [abc, ...] = ...;'.
+ if (re.search(r'\w\s+\[(?!\[)', line) and
+ not re.search(r'(?:auto&?|delete|return)\s+\[', line)):
error(filename, linenum, 'whitespace/braces', 5,
'Extra space before [')
# In range-based for, we wanted spaces before and after the colon, but
# not around "::" tokens that might appear.
- if (Search(r'for *\(.*[^:]:[^: ]', line) or
- Search(r'for *\(.*[^: ]:[^:]', line)):
+ if (re.search(r'for *\(.*[^:]:[^: ]', line) or
+ re.search(r'for *\(.*[^: ]:[^:]', line)):
error(filename, linenum, 'whitespace/forcolon', 2,
'Missing space around colon in range-based for loop')
@@ -3144,7 +3902,7 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error):
# The replacement is done repeatedly to avoid false positives from
# operators that call operators.
while True:
- match = Match(r'^(.*\boperator\b)(\S+)(\s*\(.*)$', line)
+ match = re.match(r'^(.*\boperator\b)(\S+)(\s*\(.*)$', line)
if match:
line = match.group(1) + ('_' * len(match.group(2))) + match.group(3)
else:
@@ -3154,12 +3912,12 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error):
# Otherwise not. Note we only check for non-spaces on *both* sides;
# sometimes people put non-spaces on one side when aligning ='s among
# many lines (not that this is behavior that I approve of...)
- if ((Search(r'[\w.]=', line) or
- Search(r'=[\w.]', line))
- and not Search(r'\b(if|while|for) ', line)
+ if ((re.search(r'[\w.]=', line) or
+ re.search(r'=[\w.]', line))
+ and not re.search(r'\b(if|while|for) ', line)
# Operators taken from [lex.operators] in C++11 standard.
- and not Search(r'(>=|<=|==|!=|&=|\^=|\|=|\+=|\*=|\/=|\%=)', line)
- and not Search(r'operator=', line)):
+ and not re.search(r'(>=|<=|==|!=|&=|\^=|\|=|\+=|\*=|\/=|\%=)', line)
+ and not re.search(r'operator=', line)):
error(filename, linenum, 'whitespace/operators', 4,
'Missing spaces around =')
@@ -3176,18 +3934,19 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error):
# macro context and don't do any checks. This avoids false
# positives.
#
- # Note that && is not included here. Those are checked separately
- # in CheckRValueReference
- match = Search(r'[^<>=!\s](==|!=|<=|>=|\|\|)[^<>=!\s,;\)]', line)
+ # Note that && is not included here. This is because there are too
+ # many false positives due to RValue references.
+ match = re.search(r'[^<>=!\s](==|!=|<=|>=|\|\|)[^<>=!\s,;\)]', line)
if match:
+ # TODO: support alternate operators
error(filename, linenum, 'whitespace/operators', 3,
- 'Missing spaces around %s' % match.group(1))
- elif not Match(r'#.*include', line):
+ f'Missing spaces around {match.group(1)}')
+ elif not re.match(r'#.*include', line):
# Look for < that is not surrounded by spaces. This is only
# triggered if both sides are missing spaces, even though
# technically should should flag if at least one side is missing a
# space. This is done to avoid some false positives with shifts.
- match = Match(r'^(.*[^\s<])<[^\s=<,]', line)
+ match = re.match(r'^(.*[^\s<])<[^\s=<,]', line)
if match:
(_, _, end_pos) = CloseExpression(
clean_lines, linenum, len(match.group(1)))
@@ -3198,7 +3957,7 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error):
# Look for > that is not surrounded by spaces. Similar to the
# above, we only trigger if both sides are missing spaces to avoid
# false positives with shifts.
- match = Match(r'^(.*[^-\s>])>[^\s=>,]', line)
+ match = re.match(r'^(.*[^-\s>])>[^\s=>,]', line)
if match:
(_, _, start_pos) = ReverseCloseExpression(
clean_lines, linenum, len(match.group(1)))
@@ -3211,7 +3970,7 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error):
#
# We also allow operators following an opening parenthesis, since
# those tend to be macros that deal with operators.
- match = Search(r'(operator|[^\s(<])(?:L|UL|ULL|l|ul|ull)?<<([^\s,=<])', line)
+ match = re.search(r'(operator|[^\s(<])(?:L|UL|LL|ULL|l|ul|ll|ull)?<<([^\s,=<])', line)
if (match and not (match.group(1).isdigit() and match.group(2).isdigit()) and
not (match.group(1) == 'operator' and match.group(2) == ';')):
error(filename, linenum, 'whitespace/operators', 3,
@@ -3229,16 +3988,16 @@ def CheckOperatorSpacing(filename, clean_lines, linenum, error):
# follows would be part of an identifier, and there should still be
# a space separating the template type and the identifier.
# type> alpha
- match = Search(r'>>[a-zA-Z_]', line)
+ match = re.search(r'>>[a-zA-Z_]', line)
if match:
error(filename, linenum, 'whitespace/operators', 3,
'Missing spaces around >>')
# There shouldn't be space around unary operators
- match = Search(r'(!\s|~\s|[\s]--[\s;]|[\s]\+\+[\s;])', line)
+ match = re.search(r'(!\s|~\s|[\s]--[\s;]|[\s]\+\+[\s;])', line)
if match:
error(filename, linenum, 'whitespace/operators', 4,
- 'Extra space for operator %s' % match.group(1))
+ f'Extra space for operator {match.group(1)}')
def CheckParenthesisSpacing(filename, clean_lines, linenum, error):
@@ -3253,30 +4012,29 @@ def CheckParenthesisSpacing(filename, clean_lines, linenum, error):
line = clean_lines.elided[linenum]
# No spaces after an if, while, switch, or for
- match = Search(r' (if\(|for\(|while\(|switch\()', line)
+ match = re.search(r' (if\(|for\(|while\(|switch\()', line)
if match:
error(filename, linenum, 'whitespace/parens', 5,
- 'Missing space before ( in %s' % match.group(1))
+ f'Missing space before ( in {match.group(1)}')
# For if/for/while/switch, the left and right parens should be
# consistent about how many spaces are inside the parens, and
# there should either be zero or one spaces inside the parens.
# We don't want: "if ( foo)" or "if ( foo )".
# Exception: "for ( ; foo; bar)" and "for (foo; bar; )" are allowed.
- match = Search(r'\b(if|for|while|switch)\s*'
+ match = re.search(r'\b(if|for|while|switch)\s*'
r'\(([ ]*)(.).*[^ ]+([ ]*)\)\s*{\s*$',
line)
if match:
if len(match.group(2)) != len(match.group(4)):
if not (match.group(3) == ';' and
len(match.group(2)) == 1 + len(match.group(4)) or
- not match.group(2) and Search(r'\bfor\s*\(.*; \)', line)):
+ not match.group(2) and re.search(r'\bfor\s*\(.*; \)', line)):
error(filename, linenum, 'whitespace/parens', 5,
- 'Mismatching spaces inside () in %s' % match.group(1))
+ f'Mismatching spaces inside () in {match.group(1)}')
if len(match.group(2)) not in [0, 1]:
error(filename, linenum, 'whitespace/parens', 5,
- 'Should have zero or one spaces inside ( and ) in %s' %
- match.group(1))
+ f'Should have zero or one spaces inside ( and ) in {match.group(1)}')
def CheckCommaSpacing(filename, clean_lines, linenum, error):
@@ -3301,8 +4059,9 @@ def CheckCommaSpacing(filename, clean_lines, linenum, error):
# verify that lines contain missing whitespaces, second pass on raw
# lines to confirm that those missing whitespaces are not due to
# elided comments.
- if (Search(r',[^,\s]', ReplaceAll(r'\boperator\s*,\s*\(', 'F(', line)) and
- Search(r',[^,\s]', raw[linenum])):
+ match = re.search(r',[^,\s]', re.sub(r'\b__VA_OPT__\s*\(,\)', '',
+ re.sub(r'\boperator\s*,\s*\(', 'F(', line)))
+ if (match and re.search(r',[^,\s]', raw[linenum])):
error(filename, linenum, 'whitespace/comma', 3,
'Missing space after ,')
@@ -3310,27 +4069,95 @@ def CheckCommaSpacing(filename, clean_lines, linenum, error):
# except for few corner cases
# TODO(unknown): clarify if 'if (1) { return 1;}' is requires one more
# space after ;
- if Search(r';[^\s};\\)/]', line):
+ if re.search(r';[^\s};\\)/]', line):
error(filename, linenum, 'whitespace/semicolon', 3,
'Missing space after ;')
-def CheckBracesSpacing(filename, clean_lines, linenum, error):
+def _IsType(clean_lines, nesting_state, expr):
+ """Check if expression looks like a type name, returns true if so.
+
+ Args:
+ clean_lines: A CleansedLines instance containing the file.
+ nesting_state: A NestingState instance which maintains information about
+ the current stack of nested blocks being parsed.
+ expr: The expression to check.
+ Returns:
+ True, if token looks like a type.
+ """
+ # Keep only the last token in the expression
+ last_word = re.match(r'^.*(\b\S+)$', expr)
+ if last_word:
+ token = last_word.group(1)
+ else:
+ token = expr
+
+ # Match native types and stdint types
+ if _TYPES.match(token):
+ return True
+
+ # Try a bit harder to match templated types. Walk up the nesting
+ # stack until we find something that resembles a typename
+ # declaration for what we are looking for.
+ typename_pattern = (r'\b(?:typename|class|struct)\s+' + re.escape(token) +
+ r'\b')
+ block_index = len(nesting_state.stack) - 1
+ while block_index >= 0:
+ if isinstance(nesting_state.stack[block_index], _NamespaceInfo):
+ return False
+
+ # Found where the opening brace is. We want to scan from this
+ # line up to the beginning of the function, minus a few lines.
+ # template
+ # class C
+ # : public ... { // start scanning here
+ last_line = nesting_state.stack[block_index].starting_linenum
+
+ next_block_start = 0
+ if block_index > 0:
+ next_block_start = nesting_state.stack[block_index - 1].starting_linenum
+ first_line = last_line
+ while first_line >= next_block_start:
+ if clean_lines.elided[first_line].find('template') >= 0:
+ break
+ first_line -= 1
+ if first_line < next_block_start:
+ # Didn't find any "template" keyword before reaching the next block,
+ # there are probably no template things to check for this block
+ block_index -= 1
+ continue
+
+ # Look for typename in the specified range
+ for i in range(first_line, last_line + 1, 1):
+ if re.search(typename_pattern, clean_lines.elided[i]):
+ return True
+ block_index -= 1
+
+ return False
+
+
+def CheckBracesSpacing(filename, clean_lines, linenum, nesting_state, error):
"""Checks for horizontal spacing near commas.
Args:
filename: The name of the current file.
clean_lines: A CleansedLines instance containing the file.
linenum: The number of the line to check.
+ nesting_state: A NestingState instance which maintains information about
+ the current stack of nested blocks being parsed.
error: The function to call with any errors found.
"""
line = clean_lines.elided[linenum]
# Except after an opening paren, or after another opening brace (in case of
# an initializer list, for instance), you should have spaces before your
- # braces. And since you should never have braces at the beginning of a line,
- # this is an easy test.
- match = Match(r'^(.*[^ ({>]){', line)
+ # braces when they are delimiting blocks, classes, namespaces etc.
+ # And since you should never have braces at the beginning of a line,
+ # this is an easy test. Except that braces used for initialization don't
+ # follow the same rule; we often don't want spaces before those.
+ match = re.match(r'^(.*[^ ({>]){', line)
+
if match:
# Try a bit harder to check for brace initialization. This
# happens in one of the following forms:
@@ -3360,35 +4187,40 @@ def CheckBracesSpacing(filename, clean_lines, linenum, error):
# There is a false negative with this approach if people inserted
# spurious semicolons, e.g. "if (cond){};", but we will catch the
# spurious semicolon with a separate check.
+ leading_text = match.group(1)
(endline, endlinenum, endpos) = CloseExpression(
clean_lines, linenum, len(match.group(1)))
trailing_text = ''
if endpos > -1:
trailing_text = endline[endpos:]
- for offset in xrange(endlinenum + 1,
+ for offset in range(endlinenum + 1,
min(endlinenum + 3, clean_lines.NumLines() - 1)):
trailing_text += clean_lines.elided[offset]
- if not Match(r'^[\s}]*[{.;,)<>\]:]', trailing_text):
+ # We also suppress warnings for `uint64_t{expression}` etc., as the style
+ # guide recommends brace initialization for integral types to avoid
+ # overflow/truncation.
+ if (not re.match(r'^[\s}]*[{.;,)<>\]:]', trailing_text)
+ and not _IsType(clean_lines, nesting_state, leading_text)):
error(filename, linenum, 'whitespace/braces', 5,
'Missing space before {')
# Make sure '} else {' has spaces.
- if Search(r'}else', line):
+ if re.search(r'}else', line):
error(filename, linenum, 'whitespace/braces', 5,
'Missing space before else')
# You shouldn't have a space before a semicolon at the end of the line.
# There's a special case for "for" since the style guide allows space before
# the semicolon there.
- if Search(r':\s*;\s*$', line):
+ if re.search(r':\s*;\s*$', line):
error(filename, linenum, 'whitespace/semicolon', 5,
'Semicolon defining empty statement. Use {} instead.')
- elif Search(r'^\s*;\s*$', line):
+ elif re.search(r'^\s*;\s*$', line):
error(filename, linenum, 'whitespace/semicolon', 5,
'Line contains only semicolon. If this should be an empty statement, '
'use {} instead.')
- elif (Search(r'\s+;\s*$', line) and
- not Search(r'\bfor\b', line)):
+ elif (re.search(r'\s+;\s*$', line) and
+ not re.search(r'\bfor\b', line)):
error(filename, linenum, 'whitespace/semicolon', 5,
'Extra space before last semicolon. If this should be an empty '
'statement, use {} instead.')
@@ -3407,410 +4239,10 @@ def IsDecltype(clean_lines, linenum, column):
(text, _, start_col) = ReverseCloseExpression(clean_lines, linenum, column)
if start_col < 0:
return False
- if Search(r'\bdecltype\s*$', text[0:start_col]):
+ if re.search(r'\bdecltype\s*$', text[0:start_col]):
return True
return False
-
-def IsTemplateParameterList(clean_lines, linenum, column):
- """Check if the token ending on (linenum, column) is the end of template<>.
-
- Args:
- clean_lines: A CleansedLines instance containing the file.
- linenum: the number of the line to check.
- column: end column of the token to check.
- Returns:
- True if this token is end of a template parameter list, False otherwise.
- """
- (_, startline, startpos) = ReverseCloseExpression(
- clean_lines, linenum, column)
- if (startpos > -1 and
- Search(r'\btemplate\s*$', clean_lines.elided[startline][0:startpos])):
- return True
- return False
-
-
-def IsRValueType(typenames, clean_lines, nesting_state, linenum, column):
- """Check if the token ending on (linenum, column) is a type.
-
- Assumes that text to the right of the column is "&&" or a function
- name.
-
- Args:
- typenames: set of type names from template-argument-list.
- clean_lines: A CleansedLines instance containing the file.
- nesting_state: A NestingState instance which maintains information about
- the current stack of nested blocks being parsed.
- linenum: the number of the line to check.
- column: end column of the token to check.
- Returns:
- True if this token is a type, False if we are not sure.
- """
- prefix = clean_lines.elided[linenum][0:column]
-
- # Get one word to the left. If we failed to do so, this is most
- # likely not a type, since it's unlikely that the type name and "&&"
- # would be split across multiple lines.
- match = Match(r'^(.*)(\b\w+|[>*)&])\s*$', prefix)
- if not match:
- return False
-
- # Check text following the token. If it's "&&>" or "&&," or "&&...", it's
- # most likely a rvalue reference used inside a template.
- suffix = clean_lines.elided[linenum][column:]
- if Match(r'&&\s*(?:[>,]|\.\.\.)', suffix):
- return True
-
- # Check for known types and end of templates:
- # int&& variable
- # vector&& variable
- #
- # Because this function is called recursively, we also need to
- # recognize pointer and reference types:
- # int* Function()
- # int& Function()
- if (match.group(2) in typenames or
- match.group(2) in ['char', 'char16_t', 'char32_t', 'wchar_t', 'bool',
- 'short', 'int', 'long', 'signed', 'unsigned',
- 'float', 'double', 'void', 'auto', '>', '*', '&']):
- return True
-
- # If we see a close parenthesis, look for decltype on the other side.
- # decltype would unambiguously identify a type, anything else is
- # probably a parenthesized expression and not a type.
- if match.group(2) == ')':
- return IsDecltype(
- clean_lines, linenum, len(match.group(1)) + len(match.group(2)) - 1)
-
- # Check for casts and cv-qualifiers.
- # match.group(1) remainder
- # -------------- ---------
- # const_cast< type&&
- # const type&&
- # type const&&
- if Search(r'\b(?:const_cast\s*<|static_cast\s*<|dynamic_cast\s*<|'
- r'reinterpret_cast\s*<|\w+\s)\s*$',
- match.group(1)):
- return True
-
- # Look for a preceding symbol that might help differentiate the context.
- # These are the cases that would be ambiguous:
- # match.group(1) remainder
- # -------------- ---------
- # Call ( expression &&
- # Declaration ( type&&
- # sizeof ( type&&
- # if ( expression &&
- # while ( expression &&
- # for ( type&&
- # for( ; expression &&
- # statement ; type&&
- # block { type&&
- # constructor { expression &&
- start = linenum
- line = match.group(1)
- match_symbol = None
- while start >= 0:
- # We want to skip over identifiers and commas to get to a symbol.
- # Commas are skipped so that we can find the opening parenthesis
- # for function parameter lists.
- match_symbol = Match(r'^(.*)([^\w\s,])[\w\s,]*$', line)
- if match_symbol:
- break
- start -= 1
- line = clean_lines.elided[start]
-
- if not match_symbol:
- # Probably the first statement in the file is an rvalue reference
- return True
-
- if match_symbol.group(2) == '}':
- # Found closing brace, probably an indicate of this:
- # block{} type&&
- return True
-
- if match_symbol.group(2) == ';':
- # Found semicolon, probably one of these:
- # for(; expression &&
- # statement; type&&
-
- # Look for the previous 'for(' in the previous lines.
- before_text = match_symbol.group(1)
- for i in xrange(start - 1, max(start - 6, 0), -1):
- before_text = clean_lines.elided[i] + before_text
- if Search(r'for\s*\([^{};]*$', before_text):
- # This is the condition inside a for-loop
- return False
-
- # Did not find a for-init-statement before this semicolon, so this
- # is probably a new statement and not a condition.
- return True
-
- if match_symbol.group(2) == '{':
- # Found opening brace, probably one of these:
- # block{ type&& = ... ; }
- # constructor{ expression && expression }
-
- # Look for a closing brace or a semicolon. If we see a semicolon
- # first, this is probably a rvalue reference.
- line = clean_lines.elided[start][0:len(match_symbol.group(1)) + 1]
- end = start
- depth = 1
- while True:
- for ch in line:
- if ch == ';':
- return True
- elif ch == '{':
- depth += 1
- elif ch == '}':
- depth -= 1
- if depth == 0:
- return False
- end += 1
- if end >= clean_lines.NumLines():
- break
- line = clean_lines.elided[end]
- # Incomplete program?
- return False
-
- if match_symbol.group(2) == '(':
- # Opening parenthesis. Need to check what's to the left of the
- # parenthesis. Look back one extra line for additional context.
- before_text = match_symbol.group(1)
- if linenum > 1:
- before_text = clean_lines.elided[linenum - 1] + before_text
- before_text = match_symbol.group(1)
-
- # Patterns that are likely to be types:
- # [](type&&
- # for (type&&
- # sizeof(type&&
- # operator=(type&&
- #
- if Search(r'(?:\]|\bfor|\bsizeof|\boperator\s*\S+\s*)\s*$', before_text):
- return True
-
- # Patterns that are likely to be expressions:
- # if (expression &&
- # while (expression &&
- # : initializer(expression &&
- # , initializer(expression &&
- # ( FunctionCall(expression &&
- # + FunctionCall(expression &&
- # + (expression &&
- #
- # The last '+' represents operators such as '+' and '-'.
- if Search(r'(?:\bif|\bwhile|[-+=%^(]*>)?\s*$',
- match_symbol.group(1))
- if match_func:
- # Check for constructors, which don't have return types.
- if Search(r'\b(?:explicit|inline)$', match_func.group(1)):
- return True
- implicit_constructor = Match(r'\s*(\w+)\((?:const\s+)?(\w+)', prefix)
- if (implicit_constructor and
- implicit_constructor.group(1) == implicit_constructor.group(2)):
- return True
- return IsRValueType(typenames, clean_lines, nesting_state, linenum,
- len(match_func.group(1)))
-
- # Nothing before the function name. If this is inside a block scope,
- # this is probably a function call.
- return not (nesting_state.previous_stack_top and
- nesting_state.previous_stack_top.IsBlockInfo())
-
- if match_symbol.group(2) == '>':
- # Possibly a closing bracket, check that what's on the other side
- # looks like the start of a template.
- return IsTemplateParameterList(
- clean_lines, start, len(match_symbol.group(1)))
-
- # Some other symbol, usually something like "a=b&&c". This is most
- # likely not a type.
- return False
-
-
-def IsDeletedOrDefault(clean_lines, linenum):
- """Check if current constructor or operator is deleted or default.
-
- Args:
- clean_lines: A CleansedLines instance containing the file.
- linenum: The number of the line to check.
- Returns:
- True if this is a deleted or default constructor.
- """
- open_paren = clean_lines.elided[linenum].find('(')
- if open_paren < 0:
- return False
- (close_line, _, close_paren) = CloseExpression(
- clean_lines, linenum, open_paren)
- if close_paren < 0:
- return False
- return Match(r'\s*=\s*(?:delete|default)\b', close_line[close_paren:])
-
-
-def IsRValueAllowed(clean_lines, linenum, typenames):
- """Check if RValue reference is allowed on a particular line.
-
- Args:
- clean_lines: A CleansedLines instance containing the file.
- linenum: The number of the line to check.
- typenames: set of type names from template-argument-list.
- Returns:
- True if line is within the region where RValue references are allowed.
- """
- # Allow region marked by PUSH/POP macros
- for i in xrange(linenum, 0, -1):
- line = clean_lines.elided[i]
- if Match(r'GOOGLE_ALLOW_RVALUE_REFERENCES_(?:PUSH|POP)', line):
- if not line.endswith('PUSH'):
- return False
- for j in xrange(linenum, clean_lines.NumLines(), 1):
- line = clean_lines.elided[j]
- if Match(r'GOOGLE_ALLOW_RVALUE_REFERENCES_(?:PUSH|POP)', line):
- return line.endswith('POP')
-
- # Allow operator=
- line = clean_lines.elided[linenum]
- if Search(r'\boperator\s*=\s*\(', line):
- return IsDeletedOrDefault(clean_lines, linenum)
-
- # Allow constructors
- match = Match(r'\s*(?:[\w<>]+::)*([\w<>]+)\s*::\s*([\w<>]+)\s*\(', line)
- if match and match.group(1) == match.group(2):
- return IsDeletedOrDefault(clean_lines, linenum)
- if Search(r'\b(?:explicit|inline)\s+[\w<>]+\s*\(', line):
- return IsDeletedOrDefault(clean_lines, linenum)
-
- if Match(r'\s*[\w<>]+\s*\(', line):
- previous_line = 'ReturnType'
- if linenum > 0:
- previous_line = clean_lines.elided[linenum - 1]
- if Match(r'^\s*$', previous_line) or Search(r'[{}:;]\s*$', previous_line):
- return IsDeletedOrDefault(clean_lines, linenum)
-
- # Reject types not mentioned in template-argument-list
- while line:
- match = Match(r'^.*?(\w+)\s*&&(.*)$', line)
- if not match:
- break
- if match.group(1) not in typenames:
- return False
- line = match.group(2)
-
- # All RValue types that were in template-argument-list should have
- # been removed by now. Those were allowed, assuming that they will
- # be forwarded.
- #
- # If there are no remaining RValue types left (i.e. types that were
- # not found in template-argument-list), flag those as not allowed.
- return line.find('&&') < 0
-
-
-def GetTemplateArgs(clean_lines, linenum):
- """Find list of template arguments associated with this function declaration.
-
- Args:
- clean_lines: A CleansedLines instance containing the file.
- linenum: Line number containing the start of the function declaration,
- usually one line after the end of the template-argument-list.
- Returns:
- Set of type names, or empty set if this does not appear to have
- any template parameters.
- """
- # Find start of function
- func_line = linenum
- while func_line > 0:
- line = clean_lines.elided[func_line]
- if Match(r'^\s*$', line):
- return set()
- if line.find('(') >= 0:
- break
- func_line -= 1
- if func_line == 0:
- return set()
-
- # Collapse template-argument-list into a single string
- argument_list = ''
- match = Match(r'^(\s*template\s*)<', clean_lines.elided[func_line])
- if match:
- # template-argument-list on the same line as function name
- start_col = len(match.group(1))
- _, end_line, end_col = CloseExpression(clean_lines, func_line, start_col)
- if end_col > -1 and end_line == func_line:
- start_col += 1 # Skip the opening bracket
- argument_list = clean_lines.elided[func_line][start_col:end_col]
-
- elif func_line > 1:
- # template-argument-list one line before function name
- match = Match(r'^(.*)>\s*$', clean_lines.elided[func_line - 1])
- if match:
- end_col = len(match.group(1))
- _, start_line, start_col = ReverseCloseExpression(
- clean_lines, func_line - 1, end_col)
- if start_col > -1:
- start_col += 1 # Skip the opening bracket
- while start_line < func_line - 1:
- argument_list += clean_lines.elided[start_line][start_col:]
- start_col = 0
- start_line += 1
- argument_list += clean_lines.elided[func_line - 1][start_col:end_col]
-
- if not argument_list:
- return set()
-
- # Extract type names
- typenames = set()
- while True:
- match = Match(r'^[,\s]*(?:typename|class)(?:\.\.\.)?\s+(\w+)(.*)$',
- argument_list)
- if not match:
- break
- typenames.add(match.group(1))
- argument_list = match.group(2)
- return typenames
-
-
-def CheckRValueReference(filename, clean_lines, linenum, nesting_state, error):
- """Check for rvalue references.
-
- Args:
- filename: The name of the current file.
- clean_lines: A CleansedLines instance containing the file.
- linenum: The number of the line to check.
- nesting_state: A NestingState instance which maintains information about
- the current stack of nested blocks being parsed.
- error: The function to call with any errors found.
- """
- # Find lines missing spaces around &&.
- # TODO(unknown): currently we don't check for rvalue references
- # with spaces surrounding the && to avoid false positives with
- # boolean expressions.
- line = clean_lines.elided[linenum]
- match = Match(r'^(.*\S)&&', line)
- if not match:
- match = Match(r'(.*)&&\S', line)
- if (not match) or '(&&)' in line or Search(r'\boperator\s*$', match.group(1)):
- return
-
- # Either poorly formed && or an rvalue reference, check the context
- # to get a more accurate error message. Mostly we want to determine
- # if what's to the left of "&&" is a type or not.
- typenames = GetTemplateArgs(clean_lines, linenum)
- and_pos = len(match.group(1))
- if IsRValueType(typenames, clean_lines, nesting_state, linenum, and_pos):
- if not IsRValueAllowed(clean_lines, linenum, typenames):
- error(filename, linenum, 'build/c++11', 3,
- 'RValue references are an unapproved C++ feature.')
- else:
- error(filename, linenum, 'whitespace/operators', 3,
- 'Missing spaces around &&')
-
-
def CheckSectionSpacing(filename, clean_lines, class_info, linenum, error):
"""Checks for additional blank line issues related to sections.
@@ -3838,7 +4270,7 @@ def CheckSectionSpacing(filename, clean_lines, class_info, linenum, error):
linenum <= class_info.starting_linenum):
return
- matched = Match(r'\s*(public|protected|private):', clean_lines.lines[linenum])
+ matched = re.match(r'\s*(public|protected|private):', clean_lines.lines[linenum])
if matched:
# Issue warning if the line before public/protected/private was
# not a blank line, but don't do this if the previous line contains
@@ -3850,20 +4282,20 @@ def CheckSectionSpacing(filename, clean_lines, class_info, linenum, error):
# common when defining classes in C macros.
prev_line = clean_lines.lines[linenum - 1]
if (not IsBlankLine(prev_line) and
- not Search(r'\b(class|struct)\b', prev_line) and
- not Search(r'\\$', prev_line)):
+ not re.search(r'\b(class|struct)\b', prev_line) and
+ not re.search(r'\\$', prev_line)):
# Try a bit harder to find the beginning of the class. This is to
# account for multi-line base-specifier lists, e.g.:
# class Derived
# : public Base {
end_class_head = class_info.starting_linenum
for i in range(class_info.starting_linenum, linenum):
- if Search(r'\{\s*$', clean_lines.lines[i]):
+ if re.search(r'\{\s*$', clean_lines.lines[i]):
end_class_head = i
break
if end_class_head < linenum - 1:
error(filename, linenum, 'whitespace/blank_line', 3,
- '"%s:" should be preceded by a blank line' % matched.group(1))
+ f'"{matched.group(1)}:" should be preceded by a blank line')
def GetPreviousNonBlankLine(clean_lines, linenum):
@@ -3901,31 +4333,36 @@ def CheckBraces(filename, clean_lines, linenum, error):
line = clean_lines.elided[linenum] # get rid of comments and strings
- if Match(r'\s*{\s*$', line):
+ if re.match(r'\s*{\s*$', line):
# We allow an open brace to start a line in the case where someone is using
# braces in a block to explicitly create a new scope, which is commonly used
# to control the lifetime of stack-allocated variables. Braces are also
# used for brace initializers inside function calls. We don't detect this
# perfectly: we just don't complain if the last non-whitespace character on
# the previous non-blank line is ',', ';', ':', '(', '{', or '}', or if the
- # previous line starts a preprocessor block.
+ # previous line starts a preprocessor block. We also allow a brace on the
+ # following line if it is part of an array initialization and would not fit
+ # within the 80 character limit of the preceding line.
prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0]
- if (not Search(r'[,;:}{(]\s*$', prevline) and
- not Match(r'\s*#', prevline)):
+ if (not re.search(r'[,;:}{(]\s*$', prevline) and
+ not re.match(r'\s*#', prevline) and
+ not (GetLineWidth(prevline) > _line_length - 2 and '[]' in prevline)):
error(filename, linenum, 'whitespace/braces', 4,
'{ should almost always be at the end of the previous line')
# An else clause should be on the same line as the preceding closing brace.
- if Match(r'\s*else\b\s*(?:if\b|\{|$)', line):
+ if last_wrong := re.match(r'\s*else\b\s*(?:if\b|\{|$)', line):
prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0]
- if Match(r'\s*}\s*$', prevline):
+ if re.match(r'\s*}\s*$', prevline):
error(filename, linenum, 'whitespace/newline', 4,
'An else should appear on the same line as the preceding }')
+ else:
+ last_wrong = False
# If braces come on one side of an else, they should be on both.
# However, we have to worry about "else if" that spans multiple lines!
- if Search(r'else if\s*\(', line): # could be multi-line if
- brace_on_left = bool(Search(r'}\s*else if\s*\(', line))
+ if re.search(r'else if\s*\(', line): # could be multi-line if
+ brace_on_left = bool(re.search(r'}\s*else if\s*\(', line))
# find the ( after the if
pos = line.find('else if')
pos = line.find('(', pos)
@@ -3935,19 +4372,29 @@ def CheckBraces(filename, clean_lines, linenum, error):
if brace_on_left != brace_on_right: # must be brace after if
error(filename, linenum, 'readability/braces', 5,
'If an else has a brace on one side, it should have it on both')
- elif Search(r'}\s*else[^{]*$', line) or Match(r'[^}]*else\s*{', line):
+ # Prevent detection if statement has { and we detected an improper newline after }
+ elif re.search(r'}\s*else[^{]*$', line) or (re.match(r'[^}]*else\s*{', line) and not last_wrong):
error(filename, linenum, 'readability/braces', 5,
'If an else has a brace on one side, it should have it on both')
- # Likewise, an else should never have the else clause on the same line
- if Search(r'\belse [^\s{]', line) and not Search(r'\belse if\b', line):
- error(filename, linenum, 'whitespace/newline', 4,
- 'Else clause should never be on same line as else (use 2 lines)')
-
- # In the same way, a do/while should never be on one line
- if Match(r'\s*do [^\s{]', line):
- error(filename, linenum, 'whitespace/newline', 4,
- 'do/while clauses should not be on a single line')
+ # No control clauses with braces should have its contents on the same line
+ # Exclude } which will be covered by empty-block detect
+ # Exclude ; which may be used by while in a do-while
+ if keyword := re.search(
+ r'\b(else if|if|while|for|switch)' # These have parens
+ r'\s*\(.*\)\s*(?:\[\[(?:un)?likely\]\]\s*)?{\s*[^\s\\};]', line):
+ error(filename, linenum, 'whitespace/newline', 5,
+ f'Controlled statements inside brackets of {keyword.group(1)} clause'
+ ' should be on a separate line')
+ elif keyword := re.search(
+ r'\b(else|do|try)' # These don't have parens
+ r'\s*(?:\[\[(?:un)?likely\]\]\s*)?{\s*[^\s\\}]', line):
+ error(filename, linenum, 'whitespace/newline', 5,
+ f'Controlled statements inside brackets of {keyword.group(1)} clause'
+ ' should be on a separate line')
+
+ # TODO: Err on if...else and do...while statements without braces;
+ # style guide has changed since the below comment was written
# Check single-line if/else bodies. The style guide says 'curly braces are not
# required for single-line statements'. We additionally allow multi-line,
@@ -3956,21 +4403,21 @@ def CheckBraces(filename, clean_lines, linenum, error):
# its line, and the line after that should have an indent level equal to or
# lower than the if. We also check for ambiguous if/else nesting without
# braces.
- if_else_match = Search(r'\b(if\s*\(|else\b)', line)
- if if_else_match and not Match(r'\s*#', line):
+ if_else_match = re.search(r'\b(if\s*(|constexpr)\s*\(|else\b)', line)
+ if if_else_match and not re.match(r'\s*#', line):
if_indent = GetIndentLevel(line)
endline, endlinenum, endpos = line, linenum, if_else_match.end()
- if_match = Search(r'\bif\s*\(', line)
+ if_match = re.search(r'\bif\s*(|constexpr)\s*\(', line)
if if_match:
# This could be a multiline if condition, so find the end first.
pos = if_match.end() - 1
(endline, endlinenum, endpos) = CloseExpression(clean_lines, linenum, pos)
# Check for an opening brace, either directly after the if or on the next
# line. If found, this isn't a single-statement conditional.
- if (not Match(r'\s*{', endline[endpos:])
- and not (Match(r'\s*$', endline[endpos:])
+ if (not re.match(r'\s*(?:\[\[(?:un)?likely\]\]\s*)?{', endline[endpos:])
+ and not (re.match(r'\s*$', endline[endpos:])
and endlinenum < (len(clean_lines.elided) - 1)
- and Match(r'\s*{', clean_lines.elided[endlinenum + 1]))):
+ and re.match(r'\s*{', clean_lines.elided[endlinenum + 1]))):
while (endlinenum < len(clean_lines.elided)
and ';' not in clean_lines.elided[endlinenum][endpos:]):
endlinenum += 1
@@ -3980,11 +4427,11 @@ def CheckBraces(filename, clean_lines, linenum, error):
# We allow a mix of whitespace and closing braces (e.g. for one-liner
# methods) and a single \ after the semicolon (for macros)
endpos = endline.find(';')
- if not Match(r';[\s}]*(\\?)$', endline[endpos:]):
+ if not re.match(r';[\s}]*(\\?)$', endline[endpos:]):
# Semicolon isn't the last character, there's something trailing.
# Output a warning if the semicolon is not contained inside
# a lambda expression.
- if not Match(r'^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}]*\}\s*\)*[;,]\s*$',
+ if not re.match(r'^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}]*\}\s*\)*[;,]\s*$',
endline):
error(filename, linenum, 'readability/braces', 4,
'If/else bodies with multiple statements require braces')
@@ -3995,7 +4442,7 @@ def CheckBraces(filename, clean_lines, linenum, error):
# With ambiguous nested if statements, this will error out on the
# if that *doesn't* match the else, regardless of whether it's the
# inner one or outer one.
- if (if_match and Match(r'\s*else\b', next_line)
+ if (if_match and re.match(r'\s*else\b', next_line)
and next_indent != if_indent):
error(filename, linenum, 'readability/braces', 4,
'Else clause should be indented at the same level as if. '
@@ -4019,9 +4466,9 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error):
# Block bodies should not be followed by a semicolon. Due to C++11
# brace initialization, there are more places where semicolons are
- # required than not, so we use a whitelist approach to check these
- # rather than a blacklist. These are the places where "};" should
- # be replaced by just "}":
+ # required than not, so we explicitly list the allowed rules rather
+ # than listing the disallowed ones. These are the places where "};"
+ # should be replaced by just "}":
# 1. Some flavor of block following closing parenthesis:
# for (;;) {};
# while (...) {};
@@ -4061,7 +4508,7 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error):
# to namespaces. For now we do not warn for this case.
#
# Try matching case 1 first.
- match = Match(r'^(.*\)\s*)\{', line)
+ match = re.match(r'^(.*\)\s*)\{', line)
if match:
# Matched closing parenthesis (case 1). Check the token before the
# matching opening parenthesis, and don't warn if it looks like a
@@ -4077,42 +4524,44 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error):
# - INTERFACE_DEF
# - EXCLUSIVE_LOCKS_REQUIRED, SHARED_LOCKS_REQUIRED, LOCKS_EXCLUDED:
#
- # We implement a whitelist of safe macros instead of a blacklist of
+ # We implement a list of safe macros instead of a list of
# unsafe macros, even though the latter appears less frequently in
# google code and would have been easier to implement. This is because
- # the downside for getting the whitelist wrong means some extra
- # semicolons, while the downside for getting the blacklist wrong
+ # the downside for getting the allowed checks wrong means some extra
+ # semicolons, while the downside for getting disallowed checks wrong
# would result in compile errors.
#
# In addition to macros, we also don't want to warn on
# - Compound literals
# - Lambdas
- # - alignas specifier with anonymous structs:
+ # - alignas specifier with anonymous structs
+ # - decltype
closing_brace_pos = match.group(1).rfind(')')
opening_parenthesis = ReverseCloseExpression(
clean_lines, linenum, closing_brace_pos)
if opening_parenthesis[2] > -1:
line_prefix = opening_parenthesis[0][0:opening_parenthesis[2]]
- macro = Search(r'\b([A-Z_]+)\s*$', line_prefix)
- func = Match(r'^(.*\])\s*$', line_prefix)
+ macro = re.search(r'\b([A-Z_][A-Z0-9_]*)\s*$', line_prefix)
+ func = re.match(r'^(.*\])\s*$', line_prefix)
if ((macro and
macro.group(1) not in (
'TEST', 'TEST_F', 'MATCHER', 'MATCHER_P', 'TYPED_TEST',
'EXCLUSIVE_LOCKS_REQUIRED', 'SHARED_LOCKS_REQUIRED',
'LOCKS_EXCLUDED', 'INTERFACE_DEF')) or
- (func and not Search(r'\boperator\s*\[\s*\]', func.group(1))) or
- Search(r'\b(?:struct|union)\s+alignas\s*$', line_prefix) or
- Search(r'\s+=\s*$', line_prefix)):
+ (func and not re.search(r'\boperator\s*\[\s*\]', func.group(1))) or
+ re.search(r'\b(?:struct|union)\s+alignas\s*$', line_prefix) or
+ re.search(r'\bdecltype$', line_prefix) or
+ re.search(r'\s+=\s*$', line_prefix)):
match = None
if (match and
opening_parenthesis[1] > 1 and
- Search(r'\]\s*$', clean_lines.elided[opening_parenthesis[1] - 1])):
+ re.search(r'\]\s*$', clean_lines.elided[opening_parenthesis[1] - 1])):
# Multi-line lambda-expression
match = None
else:
# Try matching cases 2-3.
- match = Match(r'^(.*(?:else|\)\s*const)\s*)\{', line)
+ match = re.match(r'^(.*(?:else|\)\s*const)\s*)\{', line)
if not match:
# Try matching cases 4-6. These are always matched on separate lines.
#
@@ -4123,14 +4572,14 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error):
# // blank line
# }
prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0]
- if prevline and Search(r'[;{}]\s*$', prevline):
- match = Match(r'^(\s*)\{', line)
+ if prevline and re.search(r'[;{}]\s*$', prevline):
+ match = re.match(r'^(\s*)\{', line)
# Check matching closing brace
if match:
(endline, endlinenum, endpos) = CloseExpression(
clean_lines, linenum, len(match.group(1)))
- if endpos > -1 and Match(r'^\s*;', endline[endpos:]):
+ if endpos > -1 and re.match(r'^\s*;', endline[endpos:]):
# Current {} pair is eligible for semicolon check, and we have found
# the redundant semicolon, output warning here.
#
@@ -4138,6 +4587,14 @@ def CheckTrailingSemicolon(filename, clean_lines, linenum, error):
# outputting warnings for the matching closing brace, if there are
# nested blocks with trailing semicolons, we will get the error
# messages in reversed order.
+
+ # We need to check the line forward for NOLINT
+ raw_lines = clean_lines.raw_lines
+ ParseNolintSuppressions(filename, raw_lines[endlinenum-1], endlinenum-1,
+ error)
+ ParseNolintSuppressions(filename, raw_lines[endlinenum], endlinenum,
+ error)
+
error(filename, endlinenum, 'readability/braces', 4,
"You don't need a ; after a }")
@@ -4159,16 +4616,16 @@ def CheckEmptyBlockBody(filename, clean_lines, linenum, error):
# We also check "if" blocks here, since an empty conditional block
# is likely an error.
line = clean_lines.elided[linenum]
- matched = Match(r'\s*(for|while|if)\s*\(', line)
+ matched = re.match(r'\s*(for|while|if)\s*\(', line)
if matched:
- # Find the end of the conditional expression
+ # Find the end of the conditional expression.
(end_line, end_linenum, end_pos) = CloseExpression(
clean_lines, linenum, line.find('('))
# Output warning if what follows the condition expression is a semicolon.
# No warning for all other cases, including whitespace or newline, since we
# have a separate check for semicolons preceded by whitespace.
- if end_pos >= 0 and Match(r';', end_line[end_pos:]):
+ if end_pos >= 0 and re.match(r';', end_line[end_pos:]):
if matched.group(1) == 'if':
error(filename, end_linenum, 'whitespace/empty_conditional_body', 5,
'Empty conditional bodies should use {}')
@@ -4176,6 +4633,75 @@ def CheckEmptyBlockBody(filename, clean_lines, linenum, error):
error(filename, end_linenum, 'whitespace/empty_loop_body', 5,
'Empty loop bodies should use {} or continue')
+ # Check for if statements that have completely empty bodies (no comments)
+ # and no else clauses.
+ if end_pos >= 0 and matched.group(1) == 'if':
+ # Find the position of the opening { for the if statement.
+ # Return without logging an error if it has no brackets.
+ opening_linenum = end_linenum
+ opening_line_fragment = end_line[end_pos:]
+ # Loop until EOF or find anything that's not whitespace or opening {.
+ while not re.search(r'^\s*\{', opening_line_fragment):
+ if re.search(r'^(?!\s*$)', opening_line_fragment):
+ # Conditional has no brackets.
+ return
+ opening_linenum += 1
+ if opening_linenum == len(clean_lines.elided):
+ # Couldn't find conditional's opening { or any code before EOF.
+ return
+ opening_line_fragment = clean_lines.elided[opening_linenum]
+ # Set opening_line (opening_line_fragment may not be entire opening line).
+ opening_line = clean_lines.elided[opening_linenum]
+
+ # Find the position of the closing }.
+ opening_pos = opening_line_fragment.find('{')
+ if opening_linenum == end_linenum:
+ # We need to make opening_pos relative to the start of the entire line.
+ opening_pos += end_pos
+ (closing_line, closing_linenum, closing_pos) = CloseExpression(
+ clean_lines, opening_linenum, opening_pos)
+ if closing_pos < 0:
+ return
+
+ # Now construct the body of the conditional. This consists of the portion
+ # of the opening line after the {, all lines until the closing line,
+ # and the portion of the closing line before the }.
+ if (clean_lines.raw_lines[opening_linenum] !=
+ CleanseComments(clean_lines.raw_lines[opening_linenum])):
+ # Opening line ends with a comment, so conditional isn't empty.
+ return
+ if closing_linenum > opening_linenum:
+ # Opening line after the {. Ignore comments here since we checked above.
+ bodylist = list(opening_line[opening_pos+1:])
+ # All lines until closing line, excluding closing line, with comments.
+ bodylist.extend(clean_lines.raw_lines[opening_linenum+1:closing_linenum])
+ # Closing line before the }. Won't (and can't) have comments.
+ bodylist.append(clean_lines.elided[closing_linenum][:closing_pos-1])
+ body = '\n'.join(bodylist)
+ else:
+ # If statement has brackets and fits on a single line.
+ body = opening_line[opening_pos+1:closing_pos-1]
+
+ # Check if the body is empty
+ if not _EMPTY_CONDITIONAL_BODY_PATTERN.search(body):
+ return
+ # The body is empty. Now make sure there's not an else clause.
+ current_linenum = closing_linenum
+ current_line_fragment = closing_line[closing_pos:]
+ # Loop until EOF or find anything that's not whitespace or else clause.
+ while re.search(r'^\s*$|^(?=\s*else)', current_line_fragment):
+ if re.search(r'^(?=\s*else)', current_line_fragment):
+ # Found an else clause, so don't log an error.
+ return
+ current_linenum += 1
+ if current_linenum == len(clean_lines.elided):
+ break
+ current_line_fragment = clean_lines.elided[current_linenum]
+
+ # The body is empty and there's no else clause until EOF or other code.
+ error(filename, end_linenum, 'whitespace/empty_if_body', 4,
+ ('If statement had no body and no else clause'))
+
def FindCheckMacro(line):
"""Find a replaceable CHECK-like macro.
@@ -4193,7 +4719,7 @@ def FindCheckMacro(line):
# to make sure that we are matching the expected CHECK macro, as
# opposed to some other macro that happens to contain the CHECK
# substring.
- matched = Match(r'^(.*\b' + macro + r'\s*)\(', line)
+ matched = re.match(r'^(.*\b' + macro + r'\s*)\(', line)
if not matched:
continue
return (macro, len(matched.group(1)))
@@ -4225,14 +4751,14 @@ def CheckCheck(filename, clean_lines, linenum, error):
# If the check macro is followed by something other than a
# semicolon, assume users will log their own custom error messages
# and don't suggest any replacements.
- if not Match(r'\s*;', last_line[end_pos:]):
+ if not re.match(r'\s*;', last_line[end_pos:]):
return
if linenum == end_line:
expression = lines[linenum][start_pos + 1:end_pos - 1]
else:
expression = lines[linenum][start_pos + 1:]
- for i in xrange(linenum + 1, end_line):
+ for i in range(linenum + 1, end_line):
expression += lines[i]
expression += last_line[0:end_pos - 1]
@@ -4243,7 +4769,7 @@ def CheckCheck(filename, clean_lines, linenum, error):
rhs = ''
operator = None
while expression:
- matched = Match(r'^\s*(<<|<<=|>>|>>=|->\*|->|&&|\|\||'
+ matched = re.match(r'^\s*(<<|<<=|>>|>>=|->\*|->|&&|\|\||'
r'==|!=|>=|>|<=|<|\()(.*)$', expression)
if matched:
token = matched.group(1)
@@ -4277,9 +4803,9 @@ def CheckCheck(filename, clean_lines, linenum, error):
# characters at once if possible. Trivial benchmark shows that this
# is more efficient when the operands are longer than a single
# character, which is generally the case.
- matched = Match(r'^([^-=!<>()&|]+)(.*)$', expression)
+ matched = re.match(r'^([^-=!<>()&|]+)(.*)$', expression)
if not matched:
- matched = Match(r'^(\s*\S)(.*)$', expression)
+ matched = re.match(r'^(\s*\S)(.*)$', expression)
if not matched:
break
lhs += matched.group(1)
@@ -4303,7 +4829,7 @@ def CheckCheck(filename, clean_lines, linenum, error):
lhs = lhs.strip()
rhs = rhs.strip()
match_constant = r'^([-+]?(\d+|0[xX][0-9a-fA-F]+)[lLuU]{0,3}|".*"|\'.*\')$'
- if Match(match_constant, lhs) or Match(match_constant, rhs):
+ if re.match(match_constant, lhs) or re.match(match_constant, rhs):
# Note: since we know both lhs and rhs, we can provide a more
# descriptive error message like:
# Consider using CHECK_EQ(x, 42) instead of CHECK(x == 42)
@@ -4313,9 +4839,8 @@ def CheckCheck(filename, clean_lines, linenum, error):
# We are still keeping the less descriptive message because if lhs
# or rhs gets long, the error message might become unreadable.
error(filename, linenum, 'readability/check', 2,
- 'Consider using %s instead of %s(a %s b)' % (
- _CHECK_REPLACEMENT[check_macro][operator],
- check_macro, operator))
+ f'Consider using {_CHECK_REPLACEMENT[check_macro][operator]}'
+ f' instead of {check_macro}(a {operator} b)')
def CheckAltTokens(filename, clean_lines, linenum, error):
@@ -4330,7 +4855,7 @@ def CheckAltTokens(filename, clean_lines, linenum, error):
line = clean_lines.elided[linenum]
# Avoid preprocessor lines
- if Match(r'^\s*#', line):
+ if re.match(r'^\s*#', line):
return
# Last ditch effort to avoid multi-line comments. This will not help
@@ -4346,8 +4871,8 @@ def CheckAltTokens(filename, clean_lines, linenum, error):
for match in _ALT_TOKEN_REPLACEMENT_PATTERN.finditer(line):
error(filename, linenum, 'readability/alt_tokens', 2,
- 'Use operator %s instead of %s' % (
- _ALT_TOKEN_REPLACEMENT[match.group(1)], match.group(1)))
+ f'Use operator {_ALT_TOKEN_REPLACEMENT[match.group(2)]}'
+ f' instead of {match.group(2)}')
def GetLineWidth(line):
@@ -4360,12 +4885,22 @@ def GetLineWidth(line):
The width of the line in column positions, accounting for Unicode
combining characters and wide characters.
"""
- if isinstance(line, unicode):
+ if isinstance(line, str):
width = 0
for uc in unicodedata.normalize('NFC', line):
if unicodedata.east_asian_width(uc) in ('W', 'F'):
width += 2
elif not unicodedata.combining(uc):
+ # Issue 337
+ # https://mail.python.org/pipermail/python-list/2012-August/628809.html
+ if (sys.version_info.major, sys.version_info.minor) <= (3, 2):
+ # https://github.com/python/cpython/blob/2.7/Include/unicodeobject.h#L81
+ is_wide_build = sysconfig.get_config_var("Py_UNICODE_SIZE") >= 4
+ # https://github.com/python/cpython/blob/2.7/Objects/unicodeobject.c#L564
+ is_low_surrogate = 0xDC00 <= ord(uc) <= 0xDFFF
+ if not is_wide_build and is_low_surrogate:
+ width -= 1
+
width += 1
return width
else:
@@ -4395,6 +4930,7 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state,
# raw strings,
raw_lines = clean_lines.lines_without_raw_strings
line = raw_lines[linenum]
+ prev = raw_lines[linenum - 1] if linenum > 0 else ''
if line.find('\t') != -1:
error(filename, linenum, 'whitespace/tab', 1,
@@ -4412,32 +4948,37 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state,
# if(match($0, " <<")) complain = 0;
# if(match(prev, " +for \\(")) complain = 0;
# if(prevodd && match(prevprev, " +for \\(")) complain = 0;
- scope_or_label_pattern = r'\s*\w+\s*:\s*\\?$'
+ scope_or_label_pattern = r'\s*(?:public|private|protected|signals)(?:\s+(?:slots\s*)?)?:\s*\\?$'
classinfo = nesting_state.InnermostClass()
initial_spaces = 0
cleansed_line = clean_lines.elided[linenum]
while initial_spaces < len(line) and line[initial_spaces] == ' ':
initial_spaces += 1
- if line and line[-1].isspace():
- error(filename, linenum, 'whitespace/end_of_line', 4,
- 'Line ends in whitespace. Consider deleting these extra spaces.')
# There are certain situations we allow one space, notably for
# section labels, and also lines containing multi-line raw strings.
- elif ((initial_spaces == 1 or initial_spaces == 3) and
- not Match(scope_or_label_pattern, cleansed_line) and
- not (clean_lines.raw_lines[linenum] != line and
- Match(r'^\s*""', line))):
+ # We also don't check for lines that look like continuation lines
+ # (of lines ending in double quotes, commas, equals, or angle brackets)
+ # because the rules for how to indent those are non-trivial.
+ if (not re.search(r'[",=><] *$', prev) and
+ (initial_spaces == 1 or initial_spaces == 3) and
+ not re.match(scope_or_label_pattern, cleansed_line) and
+ not (clean_lines.raw_lines[linenum] != line and
+ re.match(r'^\s*""', line))):
error(filename, linenum, 'whitespace/indent', 3,
'Weird number of spaces at line-start. '
'Are you using a 2-space indent?')
+ if line and line[-1].isspace():
+ error(filename, linenum, 'whitespace/end_of_line', 4,
+ 'Line ends in whitespace. Consider deleting these extra spaces.')
+
# Check if the line is a header guard.
is_header_guard = False
- if file_extension == 'h':
+ if IsHeaderExtension(file_extension):
cppvar = GetHeaderGuardCPPVariable(filename)
- if (line.startswith('#ifndef %s' % cppvar) or
- line.startswith('#define %s' % cppvar) or
- line.startswith('#endif // %s' % cppvar)):
+ if (line.startswith(f'#ifndef {cppvar}') or
+ line.startswith(f'#define {cppvar}') or
+ line.startswith(f'#endif // {cppvar}')):
is_header_guard = True
# #include lines and header guards can be long, since there's no clean way to
# split them.
@@ -4447,20 +4988,23 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state,
#
# The "$Id:...$" comment may also get very long without it being the
# developers fault.
+ #
+ # Doxygen documentation copying can get pretty long when using an overloaded
+ # function declaration
if (not line.startswith('#include') and not is_header_guard and
- not Match(r'^\s*//.*http(s?)://\S*$', line) and
- not Match(r'^// \$Id:.*#[0-9]+ \$$', line)):
+ not re.match(r'^\s*//.*http(s?)://\S*$', line) and
+ not re.match(r'^\s*//\s*[^\s]*$', line) and
+ not re.match(r'^// \$Id:.*#[0-9]+ \$$', line) and
+ not re.match(r'^\s*/// [@\\](copydoc|copydetails|copybrief) .*$', line)):
line_width = GetLineWidth(line)
- extended_length = int((_line_length * 1.25))
- if line_width > extended_length:
- error(filename, linenum, 'whitespace/line_length', 4,
- 'Lines should very rarely be longer than %i characters' %
- extended_length)
- elif line_width > _line_length:
+ if line_width > _line_length:
error(filename, linenum, 'whitespace/line_length', 2,
- 'Lines should be <= %i characters long' % _line_length)
+ f'Lines should be <= {_line_length} characters long')
if (cleansed_line.count(';') > 1 and
+ # allow simple single line lambdas
+ not re.match(r'^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}\n\r]*\}',
+ line) and
# for loops are allowed two ;'s (and may run over two lines).
cleansed_line.find('for') == -1 and
(GetPreviousNonBlankLine(clean_lines, linenum)[0].find('for') == -1 or
@@ -4476,14 +5020,12 @@ def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state,
CheckBraces(filename, clean_lines, linenum, error)
CheckTrailingSemicolon(filename, clean_lines, linenum, error)
CheckEmptyBlockBody(filename, clean_lines, linenum, error)
- CheckAccess(filename, clean_lines, linenum, nesting_state, error)
CheckSpacing(filename, clean_lines, linenum, nesting_state, error)
CheckOperatorSpacing(filename, clean_lines, linenum, error)
CheckParenthesisSpacing(filename, clean_lines, linenum, error)
CheckCommaSpacing(filename, clean_lines, linenum, error)
- CheckBracesSpacing(filename, clean_lines, linenum, error)
+ CheckBracesSpacing(filename, clean_lines, linenum, nesting_state, error)
CheckSpacingForFunctionCall(filename, clean_lines, linenum, error)
- CheckRValueReference(filename, clean_lines, linenum, nesting_state, error)
CheckCheck(filename, clean_lines, linenum, error)
CheckAltTokens(filename, clean_lines, linenum, error)
classinfo = nesting_state.InnermostClass()
@@ -4519,38 +5061,25 @@ def _DropCommonSuffixes(filename):
Returns:
The filename with the common suffix removed.
"""
- for suffix in ('test.cc', 'regtest.cc', 'unittest.cc',
- 'inl.h', 'impl.h', 'internal.h'):
+ for suffix in itertools.chain(
+ (f"{test_suffix.lstrip('_')}.{ext}"
+ for test_suffix, ext in itertools.product(_test_suffixes, GetNonHeaderExtensions())),
+ (f'{suffix}.{ext}'
+ for suffix, ext in itertools.product(['inl', 'imp', 'internal'], GetHeaderExtensions()))):
if (filename.endswith(suffix) and len(filename) > len(suffix) and
filename[-len(suffix) - 1] in ('-', '_')):
return filename[:-len(suffix) - 1]
return os.path.splitext(filename)[0]
-def _IsTestFilename(filename):
- """Determines if the given filename has a suffix that identifies it as a test.
-
- Args:
- filename: The input filename.
-
- Returns:
- True if 'filename' looks like a test, False otherwise.
- """
- if (filename.endswith('_test.cc') or
- filename.endswith('_unittest.cc') or
- filename.endswith('_regtest.cc')):
- return True
- else:
- return False
-
-
-def _ClassifyInclude(fileinfo, include, is_system):
+def _ClassifyInclude(fileinfo, include, used_angle_brackets, include_order="default"):
"""Figures out what kind of header 'include' is.
Args:
fileinfo: The current file cpplint is running over. A FileInfo instance.
include: The path to a #included file.
- is_system: True if the #include used <> rather than "".
+ used_angle_brackets: True if the #include used <> rather than "".
+ include_order: "default" or other value allowed in program arguments
Returns:
One of the _XXX_HEADER constants.
@@ -4560,6 +5089,8 @@ def _ClassifyInclude(fileinfo, include, is_system):
_C_SYS_HEADER
>>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'string', True)
_CPP_SYS_HEADER
+ >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/foo.h', True, "standardcfirst")
+ _OTHER_SYS_HEADER
>>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/foo.h', False)
_LIKELY_MY_HEADER
>>> _ClassifyInclude(FileInfo('foo/foo_unknown_extension.cc'),
@@ -4570,13 +5101,24 @@ def _ClassifyInclude(fileinfo, include, is_system):
"""
# This is a list of all standard c++ header files, except
# those already checked for above.
- is_cpp_h = include in _CPP_HEADERS
+ is_cpp_header = include in _CPP_HEADERS
+
+ # Mark include as C header if in list or in a known folder for standard-ish C headers.
+ is_std_c_header = (include_order == "default") or (include in _C_HEADERS
+ # additional linux glibc header folders
+ or re.search(rf'(?:{"|".join(C_STANDARD_HEADER_FOLDERS)})\/.*\.h', include))
+
+ # Headers with C++ extensions shouldn't be considered C system headers
+ include_ext = os.path.splitext(include)[1]
+ is_system = used_angle_brackets and include_ext not in ['.hh', '.hpp', '.hxx', '.h++']
if is_system:
- if is_cpp_h:
+ if is_cpp_header:
return _CPP_SYS_HEADER
- else:
+ if is_std_c_header:
return _C_SYS_HEADER
+ else:
+ return _OTHER_SYS_HEADER
# If the target file and the include we're checking share a
# basename when we drop common extensions, and the include
@@ -4584,9 +5126,11 @@ def _ClassifyInclude(fileinfo, include, is_system):
target_dir, target_base = (
os.path.split(_DropCommonSuffixes(fileinfo.RepositoryName())))
include_dir, include_base = os.path.split(_DropCommonSuffixes(include))
+ target_dir_pub = os.path.normpath(target_dir + '/../public')
+ target_dir_pub = target_dir_pub.replace('\\', '/')
if target_base == include_base and (
include_dir == target_dir or
- include_dir == os.path.normpath(target_dir + '/../public')):
+ include_dir == target_dir_pub):
return _LIKELY_MY_HEADER
# If the target and include share some initial basename
@@ -4628,10 +5172,12 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error):
#
# We also make an exception for Lua headers, which follow google
# naming convention but not the include convention.
- match = Match(r'#include\s*"([^/]+\.h)"', line)
- if match and not _THIRD_PARTY_HEADERS_PATTERN.match(match.group(1)):
- error(filename, linenum, 'build/include', 4,
- 'Include the directory when naming .h files')
+ match = re.match(r'#include\s*"([^/]+\.(.*))"', line)
+ if match:
+ if (IsHeaderExtension(match.group(2)) and
+ not _THIRD_PARTY_HEADERS_PATTERN.match(match.group(1))):
+ error(filename, linenum, 'build/include_subdir', 4,
+ 'Include the directory when naming header files')
# we shouldn't include a file more than once. actually, there are a
# handful of instances where doing so is okay, but in general it's
@@ -4639,17 +5185,33 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error):
match = _RE_PATTERN_INCLUDE.search(line)
if match:
include = match.group(2)
- is_system = (match.group(1) == '<')
+ used_angle_brackets = match.group(1) == '<'
duplicate_line = include_state.FindHeader(include)
if duplicate_line >= 0:
error(filename, linenum, 'build/include', 4,
- '"%s" already included at %s:%s' %
- (include, filename, duplicate_line))
- elif (include.endswith('.cc') and
+ f'"{include}" already included at {filename}:{duplicate_line}')
+ return
+
+ for extension in GetNonHeaderExtensions():
+ if (include.endswith('.' + extension) and
os.path.dirname(fileinfo.RepositoryName()) != os.path.dirname(include)):
- error(filename, linenum, 'build/include', 4,
- 'Do not include .cc files from other packages')
- elif not _THIRD_PARTY_HEADERS_PATTERN.match(include):
+ error(filename, linenum, 'build/include', 4,
+ 'Do not include .' + extension + ' files from other packages')
+ return
+
+ # We DO want to include a 3rd party looking header if it matches the
+ # filename. Otherwise we get an erroneous error "...should include its
+ # header" error later.
+ third_src_header = False
+ for ext in GetHeaderExtensions():
+ basefilename = filename[0:len(filename) - len(fileinfo.Extension())]
+ headerfile = basefilename + '.' + ext
+ headername = FileInfo(headerfile).RepositoryName()
+ if headername in include or include in headername:
+ third_src_header = True
+ break
+
+ if third_src_header or not _THIRD_PARTY_HEADERS_PATTERN.match(include):
include_state.include_list[-1].append((include, linenum))
# We want to ensure that headers appear in the right order:
@@ -4664,16 +5226,16 @@ def CheckIncludeLine(filename, clean_lines, linenum, include_state, error):
# track of the highest type seen, and complains if we see a
# lower type after that.
error_message = include_state.CheckNextIncludeOrder(
- _ClassifyInclude(fileinfo, include, is_system))
+ _ClassifyInclude(fileinfo, include, used_angle_brackets, _include_order))
if error_message:
error(filename, linenum, 'build/include_order', 4,
- '%s. Should be: %s.h, c system, c++ system, other.' %
- (error_message, fileinfo.BaseName()))
+ f'{error_message}. Should be: {fileinfo.BaseName()}.h, c system,'
+ ' c++ system, other.')
canonical_include = include_state.CanonicalizeAlphabeticalOrder(include)
if not include_state.IsInAlphabeticalOrder(
clean_lines, linenum, canonical_include):
error(filename, linenum, 'build/include_alpha', 4,
- 'Include "%s" not in alphabetical order' % include)
+ f'Include "{include}" not in alphabetical order')
include_state.SetLastHeader(canonical_include)
@@ -4703,7 +5265,7 @@ def _GetTextInside(text, start_pattern):
# Give opening punctuations to get the matching close-punctuations.
matching_punctuation = {'(': ')', '{': '}', '[': ']'}
- closing_punctuation = set(matching_punctuation.itervalues())
+ closing_punctuation = set(dict.values(matching_punctuation))
# Find the position to start extracting text.
match = re.search(start_pattern, text, re.M)
@@ -4758,6 +5320,9 @@ def _GetTextInside(text, start_pattern):
_RE_PATTERN_CONST_REF_PARAM = (
r'(?:.*\s*\bconst\s*&\s*' + _RE_PATTERN_IDENT +
r'|const\s+' + _RE_PATTERN_TYPE + r'\s*&\s*' + _RE_PATTERN_IDENT + r')')
+# Stream types.
+_RE_PATTERN_REF_STREAM_PARAM = (
+ r'(?:.*stream\s*&\s*' + _RE_PATTERN_IDENT + r')')
def CheckLanguage(filename, clean_lines, linenum, file_extension,
@@ -4790,19 +5355,17 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
# Reset include state across preprocessor directives. This is meant
# to silence warnings for conditional includes.
- match = Match(r'^\s*#\s*(if|ifdef|ifndef|elif|else|endif)\b', line)
+ match = re.match(r'^\s*#\s*(if|ifdef|ifndef|elif|else|endif)\b', line)
if match:
include_state.ResetSection(match.group(1))
- # Make Windows paths like Unix.
- fullname = os.path.abspath(filename).replace('\\', '/')
-
+
# Perform other checks now that we are sure that this is not an include line
CheckCasts(filename, clean_lines, linenum, error)
CheckGlobalStatic(filename, clean_lines, linenum, error)
CheckPrintf(filename, clean_lines, linenum, error)
- if file_extension == 'h':
+ if IsHeaderExtension(file_extension):
# TODO(unknown): check that 1-arg constructors are explicit.
# How to tell it's a constructor?
# (handled in CheckForNonStandardConstructs for now)
@@ -4812,15 +5375,15 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
# Check if people are using the verboten C basic types. The only exception
# we regularly allow is "unsigned short port" for port.
- if Search(r'\bshort port\b', line):
- if not Search(r'\bunsigned short port\b', line):
+ if re.search(r'\bshort port\b', line):
+ if not re.search(r'\bunsigned short port\b', line):
error(filename, linenum, 'runtime/int', 4,
'Use "unsigned short" for ports, not "short"')
else:
- match = Search(r'\b(short|long(?! +double)|long long)\b', line)
+ match = re.search(r'\b(short|long(?! +double)|long long)\b', line)
if match:
error(filename, linenum, 'runtime/int', 4,
- 'Use int16/int64/etc, rather than the C type %s' % match.group(1))
+ f'Use int16/int64/etc, rather than the C type {match.group(1)}')
# Check if some verboten operator overloading is going on
# TODO(unknown): catch out-of-line unary operator&:
@@ -4828,13 +5391,13 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
# int operator&(const X& x) { return 42; } // unary operator&
# The trick is it's hard to tell apart from binary operator&:
# class Y { int operator&(const Y& x) { return 23; } }; // binary operator&
- if Search(r'\boperator\s*&\s*\(\s*\)', line):
+ if re.search(r'\boperator\s*&\s*\(\s*\)', line):
error(filename, linenum, 'runtime/operator', 4,
'Unary operator& is dangerous. Do not use it.')
# Check for suspicious usage of "if" like
# } if (a == b) {
- if Search(r'\}\s*if\s*\(', line):
+ if re.search(r'\}\s*if\s*\(', line):
error(filename, linenum, 'readability/braces', 4,
'Did you mean "else if"? If not, start a new line for "if".')
@@ -4847,28 +5410,32 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
# boy_this_is_a_really_long_variable_that_cannot_fit_on_the_prev_line);
printf_args = _GetTextInside(line, r'(?i)\b(string)?printf\s*\(')
if printf_args:
- match = Match(r'([\w.\->()]+)$', printf_args)
+ match = re.match(r'([\w.\->()]+)$', printf_args)
if match and match.group(1) != '__VA_ARGS__':
function_name = re.search(r'\b((?:string)?printf)\s*\(',
line, re.I).group(1)
error(filename, linenum, 'runtime/printf', 4,
- 'Potential format string bug. Do %s("%%s", %s) instead.'
- % (function_name, match.group(1)))
+ 'Potential format string bug. Do'
+ f' {function_name}("%s", {match.group(1)}) instead.')
# Check for potential memset bugs like memset(buf, sizeof(buf), 0).
- match = Search(r'memset\s*\(([^,]*),\s*([^,]*),\s*0\s*\)', line)
- if match and not Match(r"^''|-?[0-9]+|0x[0-9A-Fa-f]$", match.group(2)):
+ match = re.search(r'memset\s*\(([^,]*),\s*([^,]*),\s*0\s*\)', line)
+ if match and not re.match(r"^''|-?[0-9]+|0x[0-9A-Fa-f]$", match.group(2)):
error(filename, linenum, 'runtime/memset', 4,
- 'Did you mean "memset(%s, 0, %s)"?'
- % (match.group(1), match.group(2)))
+ f'Did you mean "memset({match.group(1)}, 0, {match.group(2)})"?')
- if Search(r'\busing namespace\b', line):
- error(filename, linenum, 'build/namespaces', 5,
- 'Do not use namespace using-directives. '
- 'Use using-declarations instead.')
+ if re.search(r'\busing namespace\b', line):
+ if re.search(r'\bliterals\b', line):
+ error(filename, linenum, 'build/namespaces_literals', 5,
+ 'Do not use namespace using-directives. '
+ 'Use using-declarations instead.')
+ else:
+ error(filename, linenum, 'build/namespaces', 5,
+ 'Do not use namespace using-directives. '
+ 'Use using-declarations instead.')
# Detect variable-length arrays.
- match = Match(r'\s*(.+::)?(\w+) [a-z]\w*\[(.+)];', line)
+ match = re.match(r'\s*(.+::)?(\w+) [a-z]\w*\[(.+)];', line)
if (match and match.group(2) != 'return' and match.group(2) != 'delete' and
match.group(3).find(']') == -1):
# Split the size using space and arithmetic operators as delimiters.
@@ -4882,17 +5449,17 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
skip_next = False
continue
- if Search(r'sizeof\(.+\)', tok): continue
- if Search(r'arraysize\(\w+\)', tok): continue
+ if re.search(r'sizeof\(.+\)', tok): continue
+ if re.search(r'arraysize\(\w+\)', tok): continue
tok = tok.lstrip('(')
tok = tok.rstrip(')')
if not tok: continue
- if Match(r'\d+', tok): continue
- if Match(r'0[xX][0-9a-fA-F]+', tok): continue
- if Match(r'k[A-Z0-9]\w*', tok): continue
- if Match(r'(.+::)?k[A-Z0-9]\w*', tok): continue
- if Match(r'(.+::)?[A-Z][A-Z0-9_]*', tok): continue
+ if re.match(r'\d+', tok): continue
+ if re.match(r'0[xX][0-9a-fA-F]+', tok): continue
+ if re.match(r'k[A-Z0-9]\w*', tok): continue
+ if re.match(r'(.+::)?k[A-Z0-9]\w*', tok): continue
+ if re.match(r'(.+::)?[A-Z][A-Z0-9_]*', tok): continue
# A catch all for tricky sizeof cases, including 'sizeof expression',
# 'sizeof(*type)', 'sizeof(const type)', 'sizeof(struct StructName)'
# requires skipping the next token because we split on ' ' and '*'.
@@ -4909,12 +5476,12 @@ def CheckLanguage(filename, clean_lines, linenum, file_extension,
# Check for use of unnamed namespaces in header files. Registration
# macros are typically OK, so we allow use of "namespace {" on lines
# that end with backslashes.
- if (file_extension == 'h'
- and Search(r'\bnamespace\s*{', line)
+ if (IsHeaderExtension(file_extension)
+ and re.search(r'\bnamespace\s*{', line)
and line[-1] != '\\'):
- error(filename, linenum, 'build/namespaces', 4,
+ error(filename, linenum, 'build/namespaces_headers', 4,
'Do not use unnamed namespaces in header files. See '
- 'http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces'
+ 'https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces'
' for more information.')
@@ -4930,14 +5497,18 @@ def CheckGlobalStatic(filename, clean_lines, linenum, error):
line = clean_lines.elided[linenum]
# Match two lines at a time to support multiline declarations
- if linenum + 1 < clean_lines.NumLines() and not Search(r'[;({]', line):
+ if linenum + 1 < clean_lines.NumLines() and not re.search(r'[;({]', line):
line += clean_lines.elided[linenum + 1].strip()
# Check for people declaring static/global STL strings at the top level.
# This is dangerous because the C++ language does not guarantee that
- # globals with constructors are initialized before the first access.
- match = Match(
- r'((?:|static +)(?:|const +))string +([a-zA-Z0-9_:]+)\b(.*)',
+ # globals with constructors are initialized before the first access, and
+ # also because globals can be destroyed when some threads are still running.
+ # TODO(unknown): Generalize this to also find static unique_ptr instances.
+ # TODO(unknown): File bugs for clang-tidy to find these.
+ match = re.match(
+ r'((?:|static +)(?:|const +))(?::*std::)?string( +const)? +'
+ r'([a-zA-Z0-9_:]+)\b(.*)',
line)
# Remove false positives:
@@ -4957,15 +5528,19 @@ def CheckGlobalStatic(filename, clean_lines, linenum, error):
# matching identifiers.
# string Class::operator*()
if (match and
- not Search(r'\bstring\b(\s+const)?\s*\*\s*(const\s+)?\w', line) and
- not Search(r'\boperator\W', line) and
- not Match(r'\s*(<.*>)?(::[a-zA-Z0-9_]+)*\s*\(([^"]|$)', match.group(3))):
- error(filename, linenum, 'runtime/string', 4,
- 'For a static/global string constant, use a C style string instead: '
- '"%schar %s[]".' %
- (match.group(1), match.group(2)))
-
- if Search(r'\b([A-Za-z0-9_]*_)\(\1\)', line):
+ not re.search(r'\bstring\b(\s+const)?\s*[\*\&]\s*(const\s+)?\w', line) and
+ not re.search(r'\boperator\W', line) and
+ not re.match(r'\s*(<.*>)?(::[a-zA-Z0-9_]+)*\s*\(([^"]|$)', match.group(4))):
+ if re.search(r'\bconst\b', line):
+ error(filename, linenum, 'runtime/string', 4,
+ 'For a static/global string constant, use a C style string instead:'
+ f' "{match.group(1)}char{match.group(2) or ""} {match.group(3)}[]".')
+ else:
+ error(filename, linenum, 'runtime/string', 4,
+ 'Static/global string variables are not permitted.')
+
+ if (re.search(r'\b([A-Za-z0-9_]*_)\(\1\)', line) or
+ re.search(r'\b([A-Za-z0-9_]*_)\(CHECK_NOTNULL\(\1\)\)', line)):
error(filename, linenum, 'runtime/init', 4,
'You seem to be initializing a member variable with itself.')
@@ -4982,21 +5557,21 @@ def CheckPrintf(filename, clean_lines, linenum, error):
line = clean_lines.elided[linenum]
# When snprintf is used, the second argument shouldn't be a literal.
- match = Search(r'snprintf\s*\(([^,]*),\s*([0-9]*)\s*,', line)
+ match = re.search(r'snprintf\s*\(([^,]*),\s*([0-9]*)\s*,', line)
if match and match.group(2) != '0':
# If 2nd arg is zero, snprintf is used to calculate size.
- error(filename, linenum, 'runtime/printf', 3,
- 'If you can, use sizeof(%s) instead of %s as the 2nd arg '
- 'to snprintf.' % (match.group(1), match.group(2)))
+ error(filename, linenum, 'runtime/printf', 3, 'If you can, use'
+ f' sizeof({match.group(1)}) instead of {match.group(2)}'
+ ' as the 2nd arg to snprintf.')
# Check if some verboten C functions are being used.
- if Search(r'\bsprintf\s*\(', line):
+ if re.search(r'\bsprintf\s*\(', line):
error(filename, linenum, 'runtime/printf', 5,
'Never use sprintf. Use snprintf instead.')
- match = Search(r'\b(strcpy|strcat)\s*\(', line)
+ match = re.search(r'\b(strcpy|strcat)\s*\(', line)
if match:
error(filename, linenum, 'runtime/printf', 4,
- 'Almost always, snprintf is better than %s' % match.group(1))
+ f'Almost always, snprintf is better than {match.group(1)}')
def IsDerivedFunction(clean_lines, linenum):
@@ -5010,14 +5585,14 @@ def IsDerivedFunction(clean_lines, linenum):
virt-specifier.
"""
# Scan back a few lines for start of current function
- for i in xrange(linenum, max(-1, linenum - 10), -1):
- match = Match(r'^([^()]*\w+)\(', clean_lines.elided[i])
+ for i in range(linenum, max(-1, linenum - 10), -1):
+ match = re.match(r'^([^()]*\w+)\(', clean_lines.elided[i])
if match:
# Look for "override" after the matching closing parenthesis
line, _, closing_paren = CloseExpression(
clean_lines, i, len(match.group(1)))
return (closing_paren >= 0 and
- Search(r'\boverride\b', line[closing_paren:]))
+ re.search(r'\boverride\b', line[closing_paren:]))
return False
@@ -5031,9 +5606,9 @@ def IsOutOfLineMethodDefinition(clean_lines, linenum):
True if current line contains an out-of-line method definition.
"""
# Scan back a few lines for start of current function
- for i in xrange(linenum, max(-1, linenum - 10), -1):
- if Match(r'^([^()]*\w+)\(', clean_lines.elided[i]):
- return Match(r'^[^()]*\w+::\w+\(', clean_lines.elided[i]) is not None
+ for i in range(linenum, max(-1, linenum - 10), -1):
+ if re.match(r'^([^()]*\w+)\(', clean_lines.elided[i]):
+ return re.match(r'^[^()]*\w+::\w+\(', clean_lines.elided[i]) is not None
return False
@@ -5047,24 +5622,24 @@ def IsInitializerList(clean_lines, linenum):
True if current line appears to be inside constructor initializer
list, False otherwise.
"""
- for i in xrange(linenum, 1, -1):
+ for i in range(linenum, 1, -1):
line = clean_lines.elided[i]
if i == linenum:
- remove_function_body = Match(r'^(.*)\{\s*$', line)
+ remove_function_body = re.match(r'^(.*)\{\s*$', line)
if remove_function_body:
line = remove_function_body.group(1)
- if Search(r'\s:\s*\w+[({]', line):
+ if re.search(r'\s:\s*\w+[({]', line):
# A lone colon tend to indicate the start of a constructor
# initializer list. It could also be a ternary operator, which
# also tend to appear in constructor initializer lists as
# opposed to parameter lists.
return True
- if Search(r'\}\s*,\s*$', line):
+ if re.search(r'\}\s*,\s*$', line):
# A closing brace followed by a comma is probably the end of a
# brace-initialized member in constructor initializer list.
return True
- if Search(r'[{};]\s*$', line):
+ if re.search(r'[{};]\s*$', line):
# Found one of the following:
# - A closing brace or semicolon, probably the end of the previous
# function.
@@ -5128,13 +5703,13 @@ def CheckForNonConstReference(filename, clean_lines, linenum,
# that spans more than 2 lines, please use a typedef.
if linenum > 1:
previous = None
- if Match(r'\s*::(?:[\w<>]|::)+\s*&\s*\S', line):
+ if re.match(r'\s*::(?:[\w<>]|::)+\s*&\s*\S', line):
# previous_line\n + ::current_line
- previous = Search(r'\b((?:const\s*)?(?:[\w<>]|::)+[\w<>])\s*$',
+ previous = re.search(r'\b((?:const\s*)?(?:[\w<>]|::)+[\w<>])\s*$',
clean_lines.elided[linenum - 1])
- elif Match(r'\s*[a-zA-Z_]([\w<>]|::)+\s*&\s*\S', line):
+ elif re.match(r'\s*[a-zA-Z_]([\w<>]|::)+\s*&\s*\S', line):
# previous_line::\n + current_line
- previous = Search(r'\b((?:const\s*)?(?:[\w<>]|::)+::)\s*$',
+ previous = re.search(r'\b((?:const\s*)?(?:[\w<>]|::)+::)\s*$',
clean_lines.elided[linenum - 1])
if previous:
line = previous.group(1) + line.lstrip()
@@ -5148,7 +5723,7 @@ def CheckForNonConstReference(filename, clean_lines, linenum,
# Found the matching < on an earlier line, collect all
# pieces up to current line.
line = ''
- for i in xrange(startline, linenum + 1):
+ for i in range(startline, linenum + 1):
line += clean_lines.elided[i].strip()
# Check for non-const references in function parameters. A single '&' may
@@ -5172,15 +5747,15 @@ def CheckForNonConstReference(filename, clean_lines, linenum,
# appear inside the second set of parentheses on the current line as
# opposed to the first set.
if linenum > 0:
- for i in xrange(linenum - 1, max(0, linenum - 10), -1):
+ for i in range(linenum - 1, max(0, linenum - 10), -1):
previous_line = clean_lines.elided[i]
- if not Search(r'[),]\s*$', previous_line):
+ if not re.search(r'[),]\s*$', previous_line):
break
- if Match(r'^\s*:\s+\S', previous_line):
+ if re.match(r'^\s*:\s+\S', previous_line):
return
# Avoid preprocessors
- if Search(r'\\\s*$', line):
+ if re.search(r'\\\s*$', line):
return
# Avoid constructor initializer lists
@@ -5193,28 +5768,29 @@ def CheckForNonConstReference(filename, clean_lines, linenum,
#
# We also accept & in static_assert, which looks like a function but
# it's actually a declaration expression.
- whitelisted_functions = (r'(?:[sS]wap(?:<\w:+>)?|'
+ allowed_functions = (r'(?:[sS]wap(?:<\w:+>)?|'
r'operator\s*[<>][<>]|'
r'static_assert|COMPILE_ASSERT'
r')\s*\(')
- if Search(whitelisted_functions, line):
+ if re.search(allowed_functions, line):
return
- elif not Search(r'\S+\([^)]*$', line):
- # Don't see a whitelisted function on this line. Actually we
+ elif not re.search(r'\S+\([^)]*$', line):
+ # Don't see an allowed function on this line. Actually we
# didn't see any function name on this line, so this is likely a
# multi-line parameter list. Try a bit harder to catch this case.
- for i in xrange(2):
+ for i in range(2):
if (linenum > i and
- Search(whitelisted_functions, clean_lines.elided[linenum - i - 1])):
+ re.search(allowed_functions, clean_lines.elided[linenum - i - 1])):
return
- decls = ReplaceAll(r'{[^}]*}', ' ', line) # exclude function body
+ decls = re.sub(r'{[^}]*}', ' ', line) # exclude function body
for parameter in re.findall(_RE_PATTERN_REF_PARAM, decls):
- if not Match(_RE_PATTERN_CONST_REF_PARAM, parameter):
+ if (not re.match(_RE_PATTERN_CONST_REF_PARAM, parameter) and
+ not re.match(_RE_PATTERN_REF_STREAM_PARAM, parameter)):
error(filename, linenum, 'runtime/references', 2,
'Is this a non-const reference? '
'If so, make const or use a pointer: ' +
- ReplaceAll(' *<', '<', parameter))
+ re.sub(' *<', '<', parameter))
def CheckCasts(filename, clean_lines, linenum, error):
@@ -5232,8 +5808,8 @@ def CheckCasts(filename, clean_lines, linenum, error):
# I just try to capture the most common basic types, though there are more.
# Parameterless conversion functions, such as bool(), are allowed as they are
# probably a member operator declaration or default constructor.
- match = Search(
- r'(\bnew\s+|\S<\s*(?:const\s+)?)?\b'
+ match = re.search(
+ r'(\bnew\s+(?:const\s+)?|\S<\s*(?:const\s+)?)?\b'
r'(int|float|double|bool|char|int32|uint32|int64|uint64)'
r'(\([^)].*)', line)
expecting_function = ExpectingFunctionArgs(clean_lines, linenum)
@@ -5256,7 +5832,7 @@ def CheckCasts(filename, clean_lines, linenum, error):
# Avoid arrays by looking for brackets that come after the closing
# parenthesis.
- if Match(r'\([^()]+\)\s*\[', match.group(3)):
+ if re.match(r'\([^()]+\)\s*\[', match.group(3)):
return
# Other things to ignore:
@@ -5267,19 +5843,18 @@ def CheckCasts(filename, clean_lines, linenum, error):
matched_funcptr = match.group(3)
if (matched_new_or_template is None and
not (matched_funcptr and
- (Match(r'\((?:[^() ]+::\s*\*\s*)?[^() ]+\)\s*\(',
+ (re.match(r'\((?:[^() ]+::\s*\*\s*)?[^() ]+\)\s*\(',
matched_funcptr) or
matched_funcptr.startswith('(*)'))) and
- not Match(r'\s*using\s+\S+\s*=\s*' + matched_type, line) and
- not Search(r'new\(\S+\)\s*' + matched_type, line)):
+ not re.match(r'\s*using\s+\S+\s*=\s*' + matched_type, line) and
+ not re.search(r'new\(\S+\)\s*' + matched_type, line)):
error(filename, linenum, 'readability/casting', 4,
'Using deprecated casting style. '
- 'Use static_cast<%s>(...) instead' %
- matched_type)
+ f'Use static_cast<{matched_type}>(...) instead')
if not expecting_function:
CheckCStyleCast(filename, clean_lines, linenum, 'static_cast',
- r'\((int|float|double|bool|char|u?int(16|32|64))\)', error)
+ r'\((int|float|double|bool|char|u?int(16|32|64)|size_t)\)', error)
# This doesn't catch all cases. Consider (const char * const)"hello".
#
@@ -5304,7 +5879,7 @@ def CheckCasts(filename, clean_lines, linenum, error):
#
# This is not a cast:
# reference_type&(int* function_param);
- match = Search(
+ match = re.search(
r'(?:[^\w]&\(([^)*][^)]*)\)[\w(])|'
r'(?:[^\w]&(static|dynamic|down|reinterpret)_cast\b)', line)
if match:
@@ -5312,7 +5887,7 @@ def CheckCasts(filename, clean_lines, linenum, error):
# dereferenced by the casted pointer, as opposed to the casted
# pointer itself.
parenthesis_error = False
- match = Match(r'^(.*&(?:static|dynamic|down|reinterpret)_cast\b)<', line)
+ match = re.match(r'^(.*&(?:static|dynamic|down|reinterpret)_cast\b)<', line)
if match:
_, y1, x1 = CloseExpression(clean_lines, linenum, len(match.group(1)))
if x1 >= 0 and clean_lines.elided[y1][x1] == '(':
@@ -5321,7 +5896,7 @@ def CheckCasts(filename, clean_lines, linenum, error):
extended_line = clean_lines.elided[y2][x2:]
if y2 < clean_lines.NumLines() - 1:
extended_line += clean_lines.elided[y2 + 1]
- if Match(r'\s*(?:->|\[)', extended_line):
+ if re.match(r'\s*(?:->|\[)', extended_line):
parenthesis_error = True
if parenthesis_error:
@@ -5353,89 +5928,38 @@ def CheckCStyleCast(filename, clean_lines, linenum, cast_type, pattern, error):
False otherwise.
"""
line = clean_lines.elided[linenum]
- match = Search(pattern, line)
+ match = re.search(pattern, line)
if not match:
return False
# Exclude lines with keywords that tend to look like casts
context = line[0:match.start(1) - 1]
- if Match(r'.*\b(?:sizeof|alignof|alignas|[_A-Z][_A-Z0-9]*)\s*$', context):
+ if re.match(r'.*\b(?:sizeof|alignof|alignas|[_A-Z][_A-Z0-9]*)\s*$', context):
return False
# Try expanding current context to see if we one level of
# parentheses inside a macro.
if linenum > 0:
- for i in xrange(linenum - 1, max(0, linenum - 5), -1):
+ for i in range(linenum - 1, max(0, linenum - 5), -1):
context = clean_lines.elided[i] + context
- if Match(r'.*\b[_A-Z][_A-Z0-9]*\s*\((?:\([^()]*\)|[^()])*$', context):
+ if re.match(r'.*\b[_A-Z][_A-Z0-9]*\s*\((?:\([^()]*\)|[^()])*$', context):
return False
# operator++(int) and operator--(int)
- if context.endswith(' operator++') or context.endswith(' operator--'):
+ if (context.endswith(' operator++') or context.endswith(' operator--') or
+ context.endswith('::operator++') or context.endswith('::operator--')):
return False
- # A single unnamed argument for a function tends to look like old
- # style cast. If we see those, don't issue warnings for deprecated
- # casts, instead issue warnings for unnamed arguments where
- # appropriate.
- #
- # These are things that we want warnings for, since the style guide
- # explicitly require all parameters to be named:
- # Function(int);
- # Function(int) {
- # ConstMember(int) const;
- # ConstMember(int) const {
- # ExceptionMember(int) throw (...);
- # ExceptionMember(int) throw (...) {
- # PureVirtual(int) = 0;
- # [](int) -> bool {
- #
- # These are functions of some sort, where the compiler would be fine
- # if they had named parameters, but people often omit those
- # identifiers to reduce clutter:
- # (FunctionPointer)(int);
- # (FunctionPointer)(int) = value;
- # Function((function_pointer_arg)(int))
- # Function((function_pointer_arg)(int), int param)
- # ;
- # <(FunctionPointerTemplateArgument)(int)>;
+ # A single unnamed argument for a function tends to look like old style cast.
+ # If we see those, don't issue warnings for deprecated casts.
remainder = line[match.end(0):]
- if Match(r'^\s*(?:;|const\b|throw\b|final\b|override\b|[=>{),]|->)',
+ if re.match(r'^\s*(?:;|const\b|throw\b|final\b|override\b|[=>{),]|->)',
remainder):
- # Looks like an unnamed parameter.
-
- # Don't warn on any kind of template arguments.
- if Match(r'^\s*>', remainder):
- return False
-
- # Don't warn on assignments to function pointers, but keep warnings for
- # unnamed parameters to pure virtual functions. Note that this pattern
- # will also pass on assignments of "0" to function pointers, but the
- # preferred values for those would be "nullptr" or "NULL".
- matched_zero = Match(r'^\s=\s*(\S+)\s*;', remainder)
- if matched_zero and matched_zero.group(1) != '0':
- return False
-
- # Don't warn on function pointer declarations. For this we need
- # to check what came before the "(type)" string.
- if Match(r'.*\)\s*$', line[0:match.start(0)]):
- return False
-
- # Don't warn if the parameter is named with block comments, e.g.:
- # Function(int /*unused_param*/);
- raw_line = clean_lines.raw_lines[linenum]
- if '/*' in raw_line:
- return False
-
- # Passed all filters, issue warning here.
- error(filename, linenum, 'readability/function', 3,
- 'All parameters should be named in a function')
- return True
+ return False
# At this point, all that should be left is actual casts.
error(filename, linenum, 'readability/casting', 4,
- 'Using C-style cast. Use %s<%s>(...) instead' %
- (cast_type, match.group(1)))
+ f'Using C-style cast. Use {cast_type}<{match.group(1)}>(...) instead')
return True
@@ -5452,13 +5976,13 @@ def ExpectingFunctionArgs(clean_lines, linenum):
of function types.
"""
line = clean_lines.elided[linenum]
- return (Match(r'^\s*MOCK_(CONST_)?METHOD\d+(_T)?\(', line) or
+ return (re.match(r'^\s*MOCK_(CONST_)?METHOD\d+(_T)?\(', line) or
(linenum >= 2 and
- (Match(r'^\s*MOCK_(?:CONST_)?METHOD\d+(?:_T)?\((?:\S+,)?\s*$',
+ (re.match(r'^\s*MOCK_(?:CONST_)?METHOD\d+(?:_T)?\((?:\S+,)?\s*$',
clean_lines.elided[linenum - 1]) or
- Match(r'^\s*MOCK_(?:CONST_)?METHOD\d+(?:_T)?\(\s*$',
+ re.match(r'^\s*MOCK_(?:CONST_)?METHOD\d+(?:_T)?\(\s*$',
clean_lines.elided[linenum - 2]) or
- Search(r'\bstd::m?function\s*\<\s*$',
+ re.search(r'\bstd::m?function\s*\<\s*$',
clean_lines.elided[linenum - 1]))))
@@ -5483,13 +6007,16 @@ def ExpectingFunctionArgs(clean_lines, linenum):
)),
('', ('numeric_limits',)),
('', ('list',)),
- ('