Compare commits

...

No commits in common. "rspec-id-counter" and "master" have entirely different histories.

21266 changed files with 622936 additions and 1 deletions

4
.cirrus.star Normal file
View File

@ -0,0 +1,4 @@
load("github.com/SonarSource/cirrus-modules@v3", "load_features")
def main(ctx):
return load_features(ctx)

137
.cirrus.yml Normal file
View File

@ -0,0 +1,137 @@
env:
GITHUB_TOKEN: VAULT[development/github/token/${CIRRUS_REPO_OWNER}-${CIRRUS_REPO_NAME}-ro token]
SONAR_HOST_URL: VAULT[development/kv/data/next data.url]
SONAR_TOKEN: VAULT[development/kv/data/next data.token]
SONAR_SCANNER_VERSION: 5.0.1.3006
SONAR_SCANNER_HOME: ${HOME}/.sonar/sonar-scanner-${SONAR_SCANNER_VERSION}-linux
PATH: ${SONAR_SCANNER_HOME}/bin:$PATH
# Need to clone full depth to track the changed files: for SQ analysis and for validation tasks
CIRRUS_CLONE_DEPTH: 0
# Use bash (instead of sh on linux or cmd.exe on windows)
CIRRUS_SHELL: bash
BASE_BRANCH: ${CIRRUS_BASE_BRANCH}
DEFAULT_BRANCH: ${CIRRUS_DEFAULT_BRANCH}
container_definition: &CONTAINER_DEFINITION
cluster_name: ${CIRRUS_CLUSTER_NAME}
builder_role: cirrus-builder
builder_image: docker-builder-v*
builder_instance_type: t3.small
builder_subnet_id: ${CIRRUS_AWS_SUBNET}
region: eu-central-1
namespace: default
use_in_memory_disk: true
setup_sonar_scanner: &SETUP_SONAR_SCANNER
setup_sonar_scanner_script:
- apt update -y && apt upgrade -y && apt update -y && apt install -y unzip
- curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux.zip
- unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/
tooling_tests_task:
eks_container:
<<: *CONTAINER_DEFINITION
dockerfile: ci/Dockerfile
cpu: 1
memory: 2G
env:
PYTHONPATH: .
install_dependencies_script:
- ci/install_rspec_tools_dependencies.sh
tests_script:
- bash ci/fetch_branches.sh
- cd rspec-tools
- pipenv run pytest --cov=rspec_tools --cov-report=xml
<<: *SETUP_SONAR_SCANNER
analyze_script:
- cd rspec-tools
- sonar-scanner
frontend_tests_task:
eks_container:
<<: *CONTAINER_DEFINITION
dockerfile: ci/frontend-tests-dockerfile
cpu: 1
memory: 3G
node_modules_cache:
folder: frontend/node_modules
reupload_on_changes: false # since there is a fingerprint script
fingerprint_script:
- echo $CIRRUS_OS
- node --version
- cat frontend/package-lock.json
populate_script:
- cd frontend
- npm install
tests_script:
- bash ci/fetch_branches.sh
- cd frontend
- npm run build
- npm test -- --detectOpenHandles --coverage .
<<: *SETUP_SONAR_SCANNER
analyze_script:
- cd frontend
- sonar-scanner
validate_ci_tests_task:
skip: "!changesInclude('ci_tests/**', 'ci/**')"
eks_container:
<<: *CONTAINER_DEFINITION
dockerfile: ci/Dockerfile
cpu: 1
memory: 2G
ci_tests_script:
- ./ci_tests/asciidoc_validation/run_tests.sh
validate_rules_task:
eks_container:
<<: *CONTAINER_DEFINITION
dockerfile: ci/Dockerfile
cpu: 1
memory: 2G
metadata_validation_script:
- ./ci/validate_metadata.sh
file_extensions_validation_script:
- ./ci/validate_file_extensions.sh
asciidoc_validation_script:
- ./ci/validate_asciidoc.sh
validate_links_task:
timeout_in: 120m
execution_lock: RSPEC_validate_links
eks_container:
<<: *CONTAINER_DEFINITION
dockerfile: ci/Dockerfile
cpu: 1
memory: 2G
env:
LINK_CACHE_NAME: link-probing-status
LINK_CACHE_PATH: /root/link-probing-history.cache
cache_download_script:
- bash ci/cirrus-cache.sh download ${LINK_CACHE_NAME} ${LINK_CACHE_PATH}
- md5sum /root/link-probing-history.cache/link_probes.history || true
tests_script:
- ./ci/validate_links.sh ${LINK_CACHE_PATH}
always:
cache_upload_script:
- md5sum /root/link-probing-history.cache/link_probes.history || true
- bash ci/cirrus-cache.sh upload ${LINK_CACHE_NAME} ${LINK_CACHE_PATH}
all_required_checks_task:
depends_on:
- tooling_tests
- frontend_tests
- validate_rules
- validate_ci_tests
eks_container:
<<: *CONTAINER_DEFINITION
dockerfile: ci/Dockerfile
cpu: 1
memory: 1G
clone_script:
- echo 'This is a dummy task used to wait on other tasks. Clone is not necessary.'
print_message_script:
- echo 'All required checks have passed'

View File

@ -0,0 +1,16 @@
{
"build": {
"dockerfile": "../ci/Dockerfile"
},
// https://code.visualstudio.com/docs/devcontainers/create-dev-container#_rebuild
"postCreateCommand": ".devcontainer/finalize-container.sh",
"waitFor": "postCreateCommand",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"asciidoctor.asciidoctor-vscode"
]
}
}
}

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
TOP_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)/..
$TOP_DIR/ci/install_rspec_tools_dependencies.sh

1
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1 @@
.github/CODEOWNERS @sonarsource/quality-cfamily-squad

18
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,18 @@
<!--
Jira Automation:
* Mention existing issue in the PR title to move it around automatically.
* Mention existing issue in the PR description and a sub-task will be created for you to track this rspec PR separately.
No issue is created by default.
-->
## Review
A dedicated reviewer checked the rule description successfully for:
- [ ] logical errors and incorrect information
- [ ] information gaps and missing content
- [ ] text style and tone
- [ ] PR summary and labels follow [the guidelines](https://github.com/SonarSource/rspec/#to-modify-an-existing-rule)

28
.github/workflows/PullRequestClosed.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Pull Request Closed
on:
pull_request:
types: [closed]
jobs:
PullRequestMerged_job:
name: Pull Request Merged
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: read
# For external PR, ticket should be moved manually
if: |
github.event.pull_request.head.repo.full_name == github.repository
steps:
- id: secrets
uses: SonarSource/vault-action-wrapper@v3
with:
secrets: |
development/kv/data/jira user | JIRA_USER;
development/kv/data/jira token | JIRA_TOKEN;
- uses: sonarsource/gh-action-lt-backlog/PullRequestClosed@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}

View File

@ -0,0 +1,28 @@
name: Pull Request Created
on:
pull_request:
types: ["opened"]
jobs:
PullRequestCreated_job:
name: Pull Request Created
runs-on: ubuntu-latest
permissions:
id-token: write
# For external PR, ticket should be created manually
if: |
github.event.pull_request.head.repo.full_name == github.repository
steps:
- id: secrets
uses: SonarSource/vault-action-wrapper@v3
with:
secrets: |
development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN;
development/kv/data/jira user | JIRA_USER;
development/kv/data/jira token | JIRA_TOKEN;
- uses: sonarsource/gh-action-lt-backlog/PullRequestCreated@v2
with:
github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }}
jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}

28
.github/workflows/RequestReview.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Request review
on:
pull_request:
types: ["review_requested"]
jobs:
RequestReview_job:
name: Request review
runs-on: ubuntu-latest
permissions:
id-token: write
# For external PR, ticket should be moved manually
if: |
github.event.pull_request.head.repo.full_name == github.repository
steps:
- id: secrets
uses: SonarSource/vault-action-wrapper@v3
with:
secrets: |
development/github/token/{REPO_OWNER_NAME_DASH}-jira token | GITHUB_TOKEN;
development/kv/data/jira user | JIRA_USER;
development/kv/data/jira token | JIRA_TOKEN;
- uses: sonarsource/gh-action-lt-backlog/RequestReview@v2
with:
github-token: ${{ fromJSON(steps.secrets.outputs.vault).GITHUB_TOKEN }}
jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}

30
.github/workflows/SubmitReview.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Submit Review
on:
pull_request_review:
types: [submitted]
jobs:
SubmitReview_job:
name: Submit Review
runs-on: ubuntu-latest
permissions:
id-token: write
pull-requests: read
# For external PR, ticket should be moved manually
if: |
github.event.pull_request.head.repo.full_name == github.repository
&& (github.event.review.state == 'changes_requested'
|| github.event.review.state == 'approved')
steps:
- id: secrets
uses: SonarSource/vault-action-wrapper@v3
with:
secrets: |
development/kv/data/jira user | JIRA_USER;
development/kv/data/jira token | JIRA_TOKEN;
- uses: sonarsource/gh-action-lt-backlog/SubmitReview@v2
with:
github-token: ${{secrets.GITHUB_TOKEN}}
jira-user: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_USER }}
jira-token: ${{ fromJSON(steps.secrets.outputs.vault).JIRA_TOKEN }}

45
.github/workflows/add_language.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Add language to a rule
# Workflow runs when manually triggered using the UI or API.
on:
workflow_dispatch:
# Inputs the workflow accepts.
inputs:
rule:
description: 'ID of an existing rule (e.g., S1234).'
required: true
language:
description: 'Language to be added to the rule, (e.g., cfamily)'
required: true
jobs:
add_language_to_rule:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true
ref: master
path: 'rspec'
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: 'Install Pipenv'
run: |
pip install pipenv
- name: 'Install rspec-tools'
working-directory: 'rspec/rspec-tools'
run: pipenv install
- name: 'Add Language'
working-directory: 'rspec/rspec-tools'
run: pipenv run rspec-tools add-lang-to-rule --user ${{ github.actor }} --language "${{ github.event.inputs.language }}" --rule "${{ github.event.inputs.rule }}"

42
.github/workflows/create_new_rspec.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: Create New RSPEC
# Workflow runs when manually triggered using the UI or API.
on:
workflow_dispatch:
# Inputs the workflow accepts.
inputs:
languages:
description: 'Comma-separated list of targeted languages'
required: true
jobs:
create_new_rule:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: true
ref: master
path: 'rspec'
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: 'Install Pipenv'
run: |
pip install pipenv
- name: 'Install rspec-tools'
working-directory: 'rspec/rspec-tools'
run: pipenv install
- name: 'Create Rule'
working-directory: 'rspec/rspec-tools'
run: pipenv run rspec-tools create-rule --user ${{ github.actor }} --languages "${{ github.event.inputs.languages }}"

20
.github/workflows/ensure_label.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: PR should have a language label
on:
pull_request:
types: [opened, labeled, unlabeled, synchronize]
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: mheap/github-action-required-labels@v5
with:
mode: minimum
count: 1
add_comment: true
use_regex: true
labels: ".*"
message: "Please add a label with the relevant language(s) to be able to merge this PR"

38
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Build and Deploy
on:
push:
branches:
- master
- rule/**
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
pull-requests: read # Get the list and metadata of open new-rule PRs
contents: write # Get the contents of open new-rule PRs, the 'master'; write to 'gh-pages' branch
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4 # If you're using actions/checkout you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
ref: 'master'
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install libkrb5-dev -y
npm install
npm run predeploy
env:
NODE_OPTIONS: "--max-old-space-size=3048"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
SINGLE_COMMIT: true
CLEAN: true
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages # The branch the action should deploy to.
FOLDER: frontend/build # The folder the action should deploy.

141
.github/workflows/update_coverage.yml vendored Normal file
View File

@ -0,0 +1,141 @@
name: Update rule coverage
on:
schedule:
- cron: '17 2 * * *'
workflow_dispatch: # When manually triggered from a non-default branch, the results will not be pushed
jobs:
update_coverage:
runs-on: ubuntu-latest
permissions:
id-token: write # required by SonarSource/vault-action-wrapper
contents: write
actions: write # required by andymckay/cancel-action
env:
TMP_BRANCH: temporary/coverage_update
steps:
- name: 'get secrets'
id: secrets
uses: SonarSource/vault-action-wrapper@v3
with:
secrets: |
development/github/token/SonarSource-rspec-coverage token | coverage_github_token;
development/kv/data/slack token | slack_token;
- uses: actions/checkout@v4
with:
persist-credentials: true
fetch-depth: 0
path: 'rspec'
token: ${{ fromJSON(steps.secrets.outputs.vault).coverage_github_token }}
ref: 'master'
- uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: 'Install Pipenv'
run: pip install pipenv
- name: 'Install coverage script dependencies'
working-directory: 'rspec/rspec-tools'
run: |
pipenv --python python3.9 install
- name: 'Regenerate coverage information'
env:
GITHUB_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).coverage_github_token }}
id: gen-coverage
working-directory: 'rspec/rspec-tools'
run: |
pipenv run rspec-tools update-coverage --rulesdir ../rules
mv ./covered_rules.json ../frontend/public/covered_rules.json
if git diff --exit-code ../frontend/public/covered_rules.json; then
echo "new_coverage=false" >> "$GITHUB_OUTPUT"
else
echo "new_coverage=true" >> "$GITHUB_OUTPUT"
fi
- name: 'Cancel if coverage did not change'
if: steps.gen-coverage.outputs.new_coverage != 'true'
uses: andymckay/cancel-action@0.2
- name: 'Push the updated coverage file to a new branch'
id: create-temp-branch
if: steps.gen-coverage.outputs.new_coverage == 'true'
working-directory: 'rspec'
run: |
git config --global user.name "SonarTech"
git config --global user.email "sonartech@sonarsource.com"
git checkout -b $TMP_BRANCH
git add frontend/public/covered_rules.json
git commit -m "update coverage information"
git push --force-with-lease origin $TMP_BRANCH
- name: 'Create a PR'
id: create-github-pr
working-directory: 'rspec'
env:
GH_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).coverage_github_token }}
run: |
PR_URL=$(gh pr create --head ${{ env.TMP_BRANCH }} --title "Update coverage information" --body "" --label "rspec system")
gh pr merge $PR_URL
- name: 'Wait until the PR is merged'
id: wait-for-pr-to-merge
env:
GH_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).coverage_github_token }}
working-directory: 'rspec'
run: |
set -ueo pipefail
# Implicitly referring to the PR corresponding to current branch
# Set timeout (20 minutes in seconds)
TIMEOUT=1200 # seconds
START_TIME=$(date +%s)
INTERVAL=20 # seconds
while true; do
# Check if the PR is merged
PR_STATE=$(gh pr view --json state,mergedAt -q '.state')
MERGED_AT=$(gh pr view --json state,mergedAt -q '.mergedAt')
if [[ "${PR_STATE}" == "MERGED" ]]; then
echo "PR merged at: $MERGED_AT"
exit 0
fi
echo "PR state is ${PR_STATE}"
# Check for timeout
CURRENT_TIME=$(date +%s)
ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
if [[ "${ELAPSED_TIME}" -gt "${TIMEOUT}" ]]; then
echo "Timeout waiting for PR to merge."
exit 1
fi
# Wait for $INTERVAL seconds before checking again
sleep "$INTERVAL"
done
- name: 'Close PR and delete branch upon failure to merge'
if: ${{ failure() }}
env:
GH_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).coverage_github_token }}
working-directory: 'rspec'
run: |
PR_URL=$(gh pr view --json url --jq '.url')
gh pr close "$PR_URL" --delete-branch
- name: 'Notify on slack about the failure'
if: ${{ failure() }}
env:
SLACK_API_TOKEN: ${{ fromJSON(steps.secrets.outputs.vault).slack_token }}
working-directory: 'rspec/rspec-tools'
run: |
pipenv run rspec-tools notify-failure-on-slack \
--message "ERROR: failed to update rule coverage. See https://github.com/SonarSource/rspec/actions/runs/$GITHUB_RUN_ID" \
--channel team-analysis-rspec

View File

@ -0,0 +1,55 @@
name: Update quick fix status
on:
workflow_dispatch:
inputs:
rule:
description: 'ID of an existing rule (e.g., S1234).'
required: true
type: string
language:
description: 'Language to be updated for the given rule, (e.g., cfamily)'
required: true
type: string
status:
description: 'The new status for quick fix (e.g., covered)'
required: true
type: choice
options:
- covered
- partial
- targeted
- infeasible
- unknown
jobs:
update_quickfix_status:
name: Update quick fix status
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v2
with:
persist-credentials: true
ref: master
path: 'rspec'
- uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: 'Install Pipenv'
run: |
pip install pipenv
- name: 'Install rspec-tools'
working-directory: 'rspec/rspec-tools'
run: pipenv install
- name: 'Update quickfix status'
working-directory: 'rspec/rspec-tools'
run: pipenv run rspec-tools update-quickfix-status --user ${{ github.actor }} --rule "${{ github.event.inputs.rule }}" --language "${{ github.event.inputs.language }}" --status "${{ github.event.inputs.status }}"

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# IDE
.vs/
# IntelliJ IDEA
*.iws
*.iml
*.ipr
.idea/
# generated files
/rules/**/*.html
/frontend/public/rules
rspec-tools/link_probes.history
# compiled files
*.out
*.obj
*.pyc
# Eclipse
.project
.settings/

20
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"grammarly.selectors": [
{
"language": "markdown",
"scheme": "file"
},
{
"language": "asciidoc",
"scheme": "file"
}
],
"json.schemas": [
{
"fileMatch": [
"/rules/**/metadata.json"
],
"url": "./rspec-tools/rspec_tools/validation/rule-metadata-schema.json"
}
]
}

184
LICENSE Normal file
View File

@ -0,0 +1,184 @@
SONAR Source-Available License v1.0
Last Updated November 13, 2024
1. DEFINITIONS
"Agreement" means this Sonar Source-Available License v1.0
"Competing" means marketing a product or service as a substitute for the
functionality or value of SonarQube. A product or service may compete regardless
of how it is designed or deployed. For example, a product or service may compete
even if it provides its functionality via any kind of interface (including
services, libraries, or plug-ins), even if it is ported to a different platform
or programming language, and even if it is provided free of charge.
"Contribution" means:
a) in the case of the initial Contributor, the initial content Distributed under
this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
Distributed by that particular Contributor. A Contribution "originates" from a
Contributor if it was added to the Program by such Contributor itself or anyone
acting on such Contributor's behalf. Contributions do not include changes or
additions to the Program that are not Modified Works.
"Contributor" means any person or entity that Distributes the Program.
"Derivative Works" shall mean any work, whether in Source Code or other form,
that is based on (or derived from) the Program and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as a
whole, an original work of authorship.
"Distribute" means the acts of a) distributing or b) making available in any
manner that enables the transfer of a copy.
"Licensed Patents" mean patent claims licensable by a Contributor that are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Modified Works" shall mean any work in Source Code or other form that results
from an addition to, deletion from, or modification of the contents of the
Program, including, for purposes of clarity, any new file in Source Code form
that contains any contents of the Program. Modified Works shall not include
works that contain only declarations, interfaces, types, classes, structures, or
files of the Program solely in each case in order to link to, bind by name, or
subclass the Program or Modified Works thereof.
"Non-competitive Purpose" means any purpose except for (a) providing to others
any product or service that includes or offers the same or substantially similar
functionality as SonarQube, (b) Competing with SonarQube, and/or (c) employing,
using, or engaging artificial intelligence technology that is not part of the
Program to ingest, interpret, analyze, train on, or interact with the data
provided by the Program, or to engage with the Program in any manner.
"Notices" means any legal statements or attributions included with the Program,
including, without limitation, statements concerning copyright, patent,
trademark, disclaimers of warranty, or limitations of liability
"Program" means the Contributions Distributed in accordance with this Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including Contributors.
"SonarQube" means an open-source or commercial edition of software offered by
SonarSource that is branded "SonarQube".
"SonarSource" means SonarSource SA, a Swiss company registered in Switzerland
under UID No. CHE-114.587.664.
"Source Code" means the form of a Program preferred for making modifications,
including but not limited to software source code, documentation source, and
configuration files.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license, for any
Non-competitive Purpose, to reproduce, prepare Derivative Works of, publicly
display, publicly perform, Distribute and sublicense the Contribution of such
Contributor, if any, and such Derivative Works.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed
Patents, for any Non-competitive Purpose, to make, use, sell, offer to sell,
import, and otherwise transfer the Contribution of such Contributor, if any, in
Source Code or other form. This patent license shall apply to the combination of
the Contribution and the Program if, at the time the Contribution is added by
the Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any other
combinations that include the Contribution.
c) Recipient understands that although each Contributor grants the licenses to
its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other intellectual
property rights of any other entity. Each Contributor disclaims any liability to
Recipient for claims brought by any other entity based on infringement of
intellectual property rights or otherwise. As a condition to exercising the
rights and licenses granted hereunder, each Recipient hereby assumes sole
responsibility to secure any other intellectual property rights needed, if any.
For example, if a third-party patent license is required to allow Recipient to
Distribute the Program, it is Recipient's responsibility to acquire that license
before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient copyright
rights in its Contribution, if any, to grant the copyright license set forth in
this Agreement.
3. REQUIREMENTS
3.1 If a Contributor Distributes the Program in any form, then the Program must
also be made available as Source Code, in accordance with section 3.2, and the
Contributor must accompany the Program with a statement that the Source Code for
the Program is available under this Agreement, and inform Recipients how to
obtain it in a reasonable manner on or through a medium customarily used for
software exchange; and
3.2 When the Program is Distributed as Source Code:
a) it must be made available under this Agreement, and
b) a copy of this Agreement must be included with each copy of the Program.
3.3 Contributors may not remove or alter any Notices contained within the
Program from any copy of the Program which they Distribute, provided that
Contributors may add their own appropriate Notices.
4. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY
APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES
OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT
LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT,
MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
responsible for determining the appropriateness of using and distributing the
Program and assumes all risks associated with its exercise of rights under this
Agreement, including but not limited to the risks and costs of program errors,
compliance with applicable laws, damage to or loss of data, programs or
equipment, and unavailability or interruption of operations.
5. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY
APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF
THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGES.
6. GENERAL
If any provision of this Agreement is invalid or unenforceable under applicable
law, it shall not affect the validity or enforceability of the remainder of the
terms of this Agreement, and without further action by the parties hereto, such
provision shall be reformed to the minimum extent necessary to make such
provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipients patent(s), then such Recipients rights granted under
Section 2(b) shall terminate as of the date such litigation is filed.
All Recipients rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and does
not cure such failure in a reasonable period of time after becoming aware of
such noncompliance. If all Recipients rights under this Agreement terminate,
Recipient agrees to cease use and distribution of the Program as soon as
reasonably practicable. However, Recipients obligations under this Agreement
and any licenses granted by Recipient relating to the Program shall continue and
survive.
Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives
no rights or licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel, or otherwise. All rights
in the Program not expressly granted under this Agreement are reserved. Nothing
in this Agreement is intended to be enforceable by any entity that is not a
Contributor or Recipient. No third-party beneficiary rights are created under
this Agreement.

287
README.adoc Normal file
View File

@ -0,0 +1,287 @@
ifdef::env-github[]
:warning-caption: :warning:
:note-caption: :information_source:
endif::[]
= SonarSource Rule Specification repository
This repository contains the specification of every static-analysis rule available in SonarLint, SonarCloud, or SonarQube.
It also contains rules which have been dropped and rules which will one day be implemented.
The content of this repository is covered by the link:LICENSE[SONAR Source-Available License v1.0].
== Rules directory structure
* https://github.com/SonarSource/rspec/tree/master/rules[rules] directory: contains every specified rule.
** `rules/Sxxxx`: contains every specification for rule `Sxxxx`.
*** `rules/Sxxxx/*.adoc`: Asciidoc files which can be reused by multiple language-specific descriptions.
*** `rules/Sxxxx/metadata.json`: rule metadata shared between language-specific RSPECs. Each language can override fields in its own `metadata.json` file. +
It is thanks to this file that you can add `tags`, `securityStandards` etc... to your rule.
*** `rules/Sxxxx/common`: contains common content shared by all the supported languages. It is organized in the following subdirectories:
**** `rules/Sxxxx/common/fix`
**** `rules/Sxxxx/common/images`
**** `rules/Sxxxx/common/pitfalls`
**** `rules/Sxxxx/common/resources`
*** `rules/Sxxxx/[LANGUAGE]`: contains the language-specific RSPEC. For every rule, there must be at least one `[LANGUAGE]` subdirectory. +
`[LANGUAGE]` can be any of the following:
include::supported_languages.adoc[]
**** `rules/Sxxxx/[LANGUAGE]/rule.adoc`: asciidoc file used to generate the `Sxxxx` rule description for programming language `[LANGUAGE]`. It can include parts from `*.adoc` files located in the parent directory.
**** `rules/Sxxxx/[LANGUAGE]/metadata.json`: metadatas for the specific language. Each key at the top will completely override the key of the `metadata.json` file of the parent directory.
* https://github.com/SonarSource/rspec/tree/master/shared_content[shared_content] directory: contains content that needs to be shared among multiple rules. Subfolders are currently not standardized.
== Rule description file
Refer to the <<docs/description.adoc#,documentation about rules description>> for the content of the rule.adoc file.
== Metadata file
Refer to the <<docs/metadata.adoc#,documentation about the metadata file>> for the content of the metadata.json file.
== Non-content modifications
Refer to the <<docs/testing.adoc#,documentation about testing>> if you plan to change something else than rules.
== Search rules
All specified rules (implemented or not) are listed in the https://sonarsource.github.io/rspec/#/[Search Page].
For each rule, you can find the languages it covers, the descriptions for each language as well as the related open tickets and pull requests. +
There will be a red warning with a link to the rule pull request if the rule has not been implemented yet (i.e. is not present on the main branch yet).
You can also use GitHub search among unmerged PRs https://github.com/SonarSource/rspec/pulls[here].
WARNING: Unlike the Search Page, the GitHub search across the PRs for unimplemented rules considers only the PR summary and title. It does not search the content of the proposed rule (i.e. it does not look into `rule.adoc` nor into `metadata.json`).
[[AddModifyRule]]
== Create or modify a rule
Before, the Languages Team used Jira to host both implemented and unimplemented rules. This is why the `rules` directory contains both too.
However, one of the reasons we are migrating to a git repository is that we want to have a clean process and history for rule creation and modification.
In particular, the main branch aims at representing what will be integrated in the next version of the analyzers, i.e. what will be part of the next releases.
Thus every newly created rule or modification of rule should follow these steps:
. Create a pull request adding or modifying a rule
. Ask for a review
. Create an implementation ticket
. Implement the new rule or the change in the existing rule
. Fetch the updated metadata with `rule-api` by pointing it to the PR branch with `-branch` parameter
. Merge the implementation PR
. Merge the RSPEC PR
A <<multi-language-rule-creation>> is somewhat more involved.
=== 1. Create a pull request
==== To create a new rule
. go to the https://github.com/SonarSource/rspec/actions/workflows/create_new_rspec.yml[Create new RSPEC] GitHub action
. click on the grey _Run wokflow_ button (on the right).
. in the field _"Comma-separated list of targeted languages"_ write the list of languages you want to specify this rule for. +
They can be any of the following:
include::supported_languages.adoc[]
. click on the green _Run workflow_ button.
image::img/new-rule-workflow.png[]
You should see https://github.com/pulls/assigned[a new pull request assigned to you] with the appropriate language labels.
It might take up to a few minutes to appear.
It contains a scaffolding of files for the new rule. Feel free to modify it as you please.
The title of the PR for a new rule will say only "Create rule Sxxxx", which is not very informative. +
Modify the title to better summarize the nature or the rule, so that it is easier to find when searching through unimplemented rules.
Do preserve the "Create rule Sxxxx" prefix, as it is used by our tooling.
For example:
----
Create rule S7028: All identifiers should be in CamelCase
----
Add the description of the PR to further increase its discoverability
(GitHub PR search does not see the `rule.adoc`).
To do that, click on the three dots (next to the smile) on the first comment (created by github-actions bot) and select "Edit".
The rule must contain subdirectories corresponding to all the languages this rule will be implemented for.
Each language subdirectory contains the `rule.adoc` that is the root document used to render the specification.
The rule specification in `rule.adoc` can include other `*.adoc` files that are in the language subdirectory or in the parent directory by using the `include::content.adoc[]` syntax.
To reduce the number of turnarounds with the asciidoc edits you can install an asciidoc plugin.
Otherwise, you can use https://asciidoclive.com/[AsciiDocLIVE] and this https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/[cheatsheet].
==== To add language to an existing rule
Similar to adding a new rule, if the rule exists on the main branch, trigger the "Add language to a rule" GitHub action.
. go to the https://github.com/SonarSource/rspec/actions/workflows/add_language.yml[Add language to a rule] GitHub action
. click on the grey _Run workflow_ button (on the right).
. in the field _"ID of an existing rule"_ write the ID of the existing rule you want to add language to.
It must be in a form `Sxxxx` where `xxxx` is the number of the rule.
For example: `S100`, `S1234`, or `S6000`.
. in the field _"Language to be added to the rule"_ specify one language you want to add to the rule. +
It can be any of the following:
include::supported_languages.adoc[]
. click on the green _Run workflow_ button.
image::img/add-language-workflow.png[]
You should see https://github.com/pulls/assigned[a new pull request assigned to you] with the appropriate language label.
It might take up to a few minutes to appear.
It contains a scaffolding of files for the new rule. Feel free to modify it as you please.
The title of the PR for a new rule will say only "Create rule Sxxxx", which is not very informative. +
Modify the title to better summarize the nature or the rule, so that it is easier to find when searching through unimplemented rules.
Do preserve the "Create rule Sxxxx" prefix, as it is used by our tooling.
For example:
----
Create rule S100: Method names should comply with a naming convention
----
Otherwise, if the rule has not been merged yet (i.e. the rule has not been implemented by any plugin), you can use the already existing PR corresponding to this rule. +
==== To modify an existing rule
Create a branch for your modifications manually.
Then open a pull request manually.
The subject must start with the following required prefix:
----
Modify rule Sxxxx
----
Add a short summary of the PR after the required prefix. For example:
----
Modify rule S1234: Allow tail recursion for languages supporting TCO
----
Add at least one label corresponding to the language(s) the PR relates to.
==== To deprecate a rule
Create a branch for the deprecation manually.
Then open a pull request manually.
In the subject add the following text:
----
Deprecate rule Sxxxx
----
Add at least one label corresponding to the language(s) the PR relates to.
In the `metadata.json` of the rule you want to deprecate:
* Remove all tags
* Remove all quality profiles
* Change the status to `deprecated`
* Fill in the `replacementRules` array with strings in the form `"RSPEC-xxxx"` with the rules that deprecate this one, if any
See link:rules/S1212/metadata.json[S1212] for an example.
==== To delete a rule
If the rule has never been implemented and is still defined in an open pull request, just close the pull request. +
Otherwise, create a pull request to change the status of the rule to "closed".
NOTE: use a GitHub hot-key `t` in the source view to navigate to an existing rule specification.
=== 2. Ask for a review
Every PR that is creating or modifying a rule should be reviewed.
Exceptions can be made for small PRs that only resolve simple spelling mistakes.
If it is a new rule, or if it requires the analyzer to change its implementation, do not merge the pull request yet.
If the change does not require an implementation, merge the pull request after the review.
Following the submission of every PR, an automatic checklist will be generated.
This checklist is intended for the reviewer and serves as a guide to ensure comprehensive evaluation of the PR.
As the reviewer navigates through the review process, each fulfilled item on the checklist should be marked off.
A PR should not be merged before the checklist is fully completed.
This ensures that all necessary checks have been made, and all requirements have been met before merging, fostering a systematic and thorough review process.
=== 3. Create an implementation ticket
Create an implementation ticket as it is usually done for your plugin (i.e. as a Jira ticket or a Github issue). +
For this ticket to be correcty indexed on the search page of the rules, it has to contain the rule ID (RSPEC-1234 or S1234) either in the ticket title or in the ticket description. +
It is also recommended to add a link to the Github Page of the related rule, to ease the navigation between the ticket and the rule.
In the pull request adding the rule specification, add the following text referencing the implementation ticket:
----
Implementation ticket: CPP-1234 (for a CFamily ticket on Jira)
----
or
----
Implementation ticket: SonarSource/sonar-dotnet/issues/1234 (for a sonar-dotnet issue on Github)
----
=== 4. Implement the rule
Implement the rule or the modification as usual, generate the rule metadata,
and merge the rule implementation in your analyzer repository.
==== Generate rule metadata for the analyzer
* Download the last version of https://github.com/SonarSource/sonar-rule-api[rule-api].
* Run `generate` and specify the RSPEC-repository branch with the modified version of the rule specification.
Example:
[source,shell]
----
$ java -jar rule-api-2.1.0.jar generate -branch <RSPEC branch> -rule S4328
----
=== 5. Merge the RSPEC PR
Once the corresponding implementation is done you can merge the PR containing the new rule
(or the new version of the existing rule).
=== Multi-Language Rule Creation
Multi-language rule creation has more steps than the default process because it involves multiple roles that typically do not coincide.
It is infeasible to synchronize the implementation of the rule for all the languages it covers.
. An RSPECator creates a PR and specifies the multi-language rule.
* The RSPECator asks for a review for the PR.
* The RSPECator does not merge the PR, even after the review is done.
. The RSPECator opens implementation tickets for all the targeted languages.
. An Ada analyzer developer Alice implements the rule first. Alice prepares the PR with the implementation.
. As soon as the implementation of the rule is ready for Ada analyzer, Alice merges both PRs:
.. Alice fetches the rule metadata with `rule-api` into Ada analyzer. She needs to specify the PR branch in the `-branch` argument of `rule-api`.
.. Alice merges the rule implementation in Ada analyzer.
.. Alice merges the RSPEC PR opened by the RSPECator.
. A Cobol analyzer developer Bob implements the rule some time later. Bob prepares the PR with the implementation.
. As soon as the Cobol implementation is ready:
.. If Bob needs to change the rule specification, he opens an RSPEC PR and uses the PR branch in the `-branch` argument of `rule-api`.
.. Bob fetches the rule metadata with `rule-api` into Cobol analyzer.
.. Bob merges the rule implementation in Cobol analyzer.
.. If Bob had opened an RSPEC PR with changes, he merges it once the rule is implemented in the analyzer.
== Untriaged Pull Requests
Untriaged PRs are the ones without any GitHub label assigned to them.
You can easily see all of them with the filter https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+no%3Alabel[`no:label`].
All triaged PRs should have at least one label that corresponds to the bubble(s) the PR is related to.
This allows bubbles to easily filter the PRs they are interested in.
== Tooling
https://github.com/SonarSource/rspec/tree/master/rspec-tools[rspec-tools]::
A python CLI tool for adding and validating rules. It is used by GitHub checks and GitHub actions.
For more information see the README file in the `rspec-tools` directory.
https://github.com/SonarSource/rspec/tree/master/frontend[frontend]::
The GitHub page that enables the search for rules.
For more information see the README file in the `frontend` directory.
== RSPEC dataflow
The following graph shows the path of an RSPEC from its inception in Github RSPEC repository to its consumption in SQ/SC/SL or on rules.sonarsource.com: +
(The part that is grayed out corresponds to what existed before, when RSPECs were hosted in Jira)
image::img/RSPEC-flow-2.png[]
== Help
Tickets related to this RSPEC repository are in Jira, in the https://jira.sonarsource.com/projects/RULEAPI/issues/RULEAPI-324?filter=allopenissues[RULEAPI] project.
You found a bug, something is bothering you or you have an idea of how to improve the project? First, have a look at all the https://jira.sonarsource.com/projects/RULEAPI/issues/RULEAPI-324?filter=allopenissues[open tickets]. If you don't see anything related to your subject, please open a new ticket in the backlog, with `backlog` as the fix version.

17
ci/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM public.ecr.aws/docker/library/python:3.9-slim-buster
# Also install NodeJS 16 to run Sonar analysis
RUN apt-get update && \
apt-get install -y --no-install-recommends jq php-json-schema asciidoctor pipenv git curl \
ca-certificates gnupg && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \
gpg --dearmor -o /etc/apt/nodesource-keyring.gpg && \
echo "deb [signed-by=/etc/apt/nodesource-keyring.gpg] https://deb.nodesource.com/node_20.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list && \
apt-get update && \
apt-get -y install nodejs && \
rm -rf /var/lib/apt/lists/*
CMD ["bash"]

View File

@ -0,0 +1,165 @@
#!/usr/bin/env ruby
# Based on asciidoctor main ruby script.
# This is only meant to introspect and log which asciidoc files were used.
require 'asciidoctor'
require 'asciidoctor/cli'
MAIN_FILE_REGEX = /^.*\/rules\/(?<id>S\d+)\/(?:(?<lang>\w+)\/)?rule.adoc$/
class MainFileLogger < Asciidoctor::Extensions::Preprocessor
include Asciidoctor::Logging
def process document, reader
# Enable sourcemap to track source location.
# This is useful to report more accurate errors in other loggers.
document.sourcemap = true
main_file = document.normalize_system_path(reader.file, document.reader.dir)
# This assumes unix-style path separator.
if nil == main_file.match(MAIN_FILE_REGEX)
abort("Main file does not follow expected pattern: #{main_file}")
end
logger.info("ASCIIDOC LOGGER MAIN FILE:#{main_file}")
reader
end
end
class IncludeLogger < Asciidoctor::Extensions::IncludeProcessor
include Asciidoctor::Logging
def initialize document
@config = {} # Defined in parent class; will be updated by the extension registry mechanism.
@document = document
# @document.reader.file is not defined yet at this stage.
# Therefore we cannot compute the main file path and cache it.
# This cannot be done once in handles? because the object is then frozen.
# For these reasons, we end up recomputing the rule directory path each time.
end
def get_main_file reader
# See how include_stack is used:
# https://github.com/asciidoctor/asciidoctor/blob/f3800cc9c92faf8370041b2b27a61124318ed289/lib/asciidoctor/reader.rb#L669
if reader.include_stack.empty?
reader.file
else
main_frame = reader.include_stack.fetch(0)
main_frame.fetch(1)
end
end
def handles? target
include_path = @document.normalize_system_path(target, @document.reader.dir)
main_file = get_main_file(@document.reader)
main_file = @document.normalize_system_path(main_file, @document.reader.dir)
rule_dir = File.dirname(File.dirname(main_file))
rule_id = File.basename(rule_dir)
if rule_id == 'rules'
# This is a language-agnostic rule description.
rule_dir = File.dirname(main_file)
rule_id = File.basename(rule_dir)
end
git_dir = File.dirname(File.dirname(rule_dir))
shared_dir = File.join(git_dir, 'shared_content')
rule_dir = rule_dir + '/' # Don't allow S100 to include things from S1000.
if !include_path.start_with?(rule_dir) && !include_path.start_with?(shared_dir)
logger.info("ASCIIDOC LOGGER CROSSREFERENCE:#{rule_id} cross-references #{include_path}")
end
logger.info("ASCIIDOC LOGGER INCLUDE:#{include_path}")
false # Actually, this include processor does nothing.
end
# Intentionnaly no process function here.
end
class SourceLogger < Asciidoctor::Extensions::TreeProcessor
include Asciidoctor::Logging
def get_source_location block
loc = block.source_location # Asciidoctor::Reader::Cursor.
return "#{loc.file}:#{loc.lineno}"
end
def get_rule document
main_file = document.normalize_system_path(document.reader.file, document.reader.dir)
res = main_file.match(MAIN_FILE_REGEX)
lang = res[:lang] || 'default'
"#{res[:id]}/#{lang}"
end
def process document
rule = get_rule(document)
source_blocks = document.find_by(context: :listing, style: 'source')
# Collect individually valid blocks per diff-id.
blocks_per_id = Hash.new { |hash, key| hash[key] = Array.new }
# Find blocks with only diff-id but no diff-type, or vice-versa, or invalid diff-type.
source_blocks.each do |block|
id = block.attr('diff-id', nil)
type = block.attr('diff-type', nil)
if !id and !type
next # Nothing to validate
end
loc = get_source_location(block)
if !id
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id is missing in #{loc}")
next
end
if !type
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-type is missing in #{loc}")
next
elsif !['compliant', 'noncompliant'].include?(type)
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-type '#{type}' is not valid in #{loc}")
next
end
# The block is valid on its own.
blocks_per_id[id].push(block)
end
# Each diff-id should have:
# * exactly 1 noncompliant block, and
# * 1 or more compliant blocks.
# Find blocks that break this rule.
blocks_per_id.each do |id, blocks|
# Sort to ensure deterministic output.
blocks.sort_by! { |block| get_source_location(block) }
locs = blocks.map { |block| get_source_location(block) }.join(', ')
compliant = blocks.count { |block| block.attr('diff-type') == 'compliant' }
if compliant == 0
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id=#{id} has zero compliant example: #{locs}")
end
noncompliant = blocks.count { |block| block.attr('diff-type') == 'noncompliant' }
if noncompliant != 1
message = noncompliant == 0 ? "zero noncompliant example" : "too many noncompliant examples"
logger.info("ASCIIDOC LOGGER DIFF:[#{rule}] diff-id=#{id} has #{message}: #{locs}")
end
end
end
end
Asciidoctor::Extensions.register do
preprocessor MainFileLogger
include_processor IncludeLogger.new @document
treeprocessor SourceLogger
end
invoker = Asciidoctor::Cli::Invoker.new ARGV
GC.start
invoker.invoke!
exit invoker.code

View File

@ -0,0 +1,86 @@
#!/usr/bin/env bash
#
# Validate file inclusion and cross-references.
#
# This script is parametrized by this environment variable:
# * TOPLEVEL: $TOPLEVEL/rules and $TOPLEVEL/shared_content will be validated.
#
# The script exists with a non-zero value when an unexpected error happened or
# there are asciidoc validation errors.
#
# Validation errors are reported on stderr.
#
# Implementation details:
#
# The validation of file inclusion and cross-references needs to be done
# on all rule descriptions, including the default, language-agnostic
# description, with rspecator-view. Otherwise, a rule could drop an include
# of a shared_content asciidoc and that file could become unused.
#
# We use a custom asciidoctor with extra logging for this purpose.
# The format for the interesting log entries are:
# asciidoctor: INFO: ASCIIDOC LOGGER MAIN FILE: $PATH
# asciidoctor: INFO: ASCIIDOC LOGGER INCLUDED: $PATH
# asciidoctor: INFO: ASCIIDOC LOGGER CROSSREFERENCE: $RULEID cross-references $PATH
# asciidoctor: INFO: ASCIIDOC LOGGER DIFF: $VALIDATION_FAILURE_MESSAGE
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
TOPLEVEL="$(realpath "${TOPLEVEL}")"
RULES_DIR="${TOPLEVEL}/rules"
SHARED_CONTENT_DIR="${TOPLEVEL}/shared_content"
TMPOUT_DIR="$(mktemp -d)"
exit_code=0
grep_nofail() {
# Grep but always exit with 0 when there are no matches.
# Yet, exit with non-zero if an error occured.
grep "$@" || [ "$?" == "1" ]
}
extract_messages_from_log() {
# The first 3 columns of the log are not relevant.
# The 4th (and any that follows if ':' is used in the message)
# provides the relevant validation error message.
cut -d ':' -f 4- | sort -u
}
find "${RULES_DIR}" -name 'rule.adoc' \
| xargs "${SCRIPT_DIR}/custom-asciidoctor" -a rspecator-view --verbose -R "${RULES_DIR}" -D "${TMPOUT_DIR}" 2>&1 \
| grep_nofail -e 'ASCIIDOC LOGGER' \
> "${TMPOUT_DIR}/asciidoc_introspection"
cross_references=$(grep_nofail -e 'CROSSREFERENCE' "${TMPOUT_DIR}/asciidoc_introspection" | extract_messages_from_log)
if [[ -n "$cross_references" ]]; then
echo >&2 'ERROR: Some rules try to include content from unallowed directories.'
echo >&2 'To share content between rules, you should use the "shared_content" folder at the root of the repository.'
echo >&2 'List of errors:'
echo >&2 "${cross_references}"
exit_code=1
fi
grep_nofail -Pe '(INCLUDE|MAIN FILE)' "${TMPOUT_DIR}/asciidoc_introspection" \
| extract_messages_from_log \
> "${TMPOUT_DIR}/used_asciidoc_files"
git ls-files --cached -- "${RULES_DIR}/**.adoc" "${SHARED_CONTENT_DIR}/**.adoc" \
| xargs realpath \
> "${TMPOUT_DIR}/all_asciidoc_files"
orphans=$(comm -1 -3 <(sort -u "${TMPOUT_DIR}/used_asciidoc_files") <(sort -u "${TMPOUT_DIR}/all_asciidoc_files"))
if [[ -n "$orphans" ]]
then
printf >&2 'ERROR: These adoc files are not included anywhere:\n-----\n%s\n-----\n' "$orphans"
exit_code=1
fi
bad_diffs=$(grep_nofail -e 'DIFF' "${TMPOUT_DIR}/asciidoc_introspection" | extract_messages_from_log)
if [[ -n "$bad_diffs" ]]
then
printf >&2 'ERROR: Diff highlighting is used incorrectly:\n-----\n%s\n-----\n' "$bad_diffs"
exit_code=1
fi
exit $exit_code

43
ci/cirrus-cache.sh Normal file
View File

@ -0,0 +1,43 @@
#! /bin/bash
set -euo pipefail
ACTION=${1}
CACHE_NAME=${2}
PATH_TO_CACHE=${3}
CACHE_URL="http://${CIRRUS_HTTP_CACHE_HOST}/${CACHE_NAME}"
TMP_PATH="/tmp/tmp-cache.tgz"
case "${ACTION}" in
download)
echo "Download cache with key ${CACHE_NAME} from ${CACHE_URL}"
curl --silent --show-error --fail --location --output "${TMP_PATH}" "${CACHE_URL}" || {
echo "Cache download failed" >&2
exit 0
}
du -hs "${TMP_PATH}"
tar -Pxzf "${TMP_PATH}"
rm "${TMP_PATH}"
;;
upload)
echo "Upload cache to ${CACHE_URL}"
tar -Pczf "${TMP_PATH}" "${PATH_TO_CACHE}"
du -hs "${TMP_PATH}"
curl --silent --show-error -X POST --data-binary "@${TMP_PATH}" "${CACHE_URL}" || {
echo "Cache upload failed" >&2
exit 0
}
;;
*)
echo "Unexpected cache ACTION: ${ACTION}" >&2
exit 1
;;
esac
echo "Cache ${ACTION}ed succeeded."

7
ci/fetch_branches.sh Normal file
View File

@ -0,0 +1,7 @@
#!/bin/bash
set -euo pipefail
# When neither BASE_BRANCH nor DEFAULT_BRANCH are defined, fall back to "master".
BRANCH="${BASE_BRANCH:-${DEFAULT_BRANCH:-master}}"
git fetch --force origin "refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}"

View File

@ -0,0 +1,3 @@
FROM public.ecr.aws/docker/library/node:20.9.0
CMD ["bash"]

8
ci/generate_html.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
set -euo pipefail
mkdir -p out
asciidoctor -R rules -D out 'rules/*/*/rule.adoc' -q
cd rules
find . -name 'metadata.json' -exec cp --parents '{}' ../out \;
cd ..

View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
cd rspec-tools
pipenv install --dev
pipenv run pip install pytest pytest-cov

149
ci/validate_asciidoc.sh Executable file
View File

@ -0,0 +1,149 @@
#!/bin/bash
set -uo pipefail
# Install script dependencies
set -e
cd rspec-tools && pipenv install && cd ..
set +e
# This script runs all tests; it doesn't exit at the first failure.
exit_code=0
readonly ALLOWED_RULE_SUB_FOLDERS=['common'];
# Validate user-visible rule descriptions
# i.e., without rspecator-view.
./ci/generate_html.sh
cd rspec-tools
if pipenv run rspec-tools check-description --d ../out; then
echo "Rule descriptions are fine"
else
echo "ERROR: There are invalid rule descriptions"
exit_code=1
fi
cd ..
# Compute the set of affected rules
git fetch origin "$CIRRUS_DEFAULT_BRANCH"
branch_base_sha=$(git merge-base FETCH_HEAD HEAD)
echo "Comparing against the merge-base: $branch_base_sha"
changeset=$(git diff --name-only "$branch_base_sha"..HEAD)
affected_rules=$(printf '%s\n' "$changeset" | grep '/S[0-9]\+/' | sed 's:\(.*/S[0-9]\+\)/.*:\1:' | sort | uniq)
if printf '%s\n' "$changeset" | grep -qv '/S[0-9]\+/'; then
echo "Some rpec tools or shared_content changed, validating all rules"
affected_rules=rules/*
fi
# Validate some properties of the asciidoc:
#
# [properties validated only on affected rules]
# * Rules should have at least one language specification,
# unless they are closed or deprecated.
# * The include:: should have an empty line before and after them.
# * Only valid languages can be used as subdirectories in rule directories,
# with the exception of ALLOWED_RULE_SUB_FOLDERS.
# * Asciidoc files are free or errors and warnings.
# * ifdef/endif are used appropriatedly.
#
# [properties validated always on all rules]
# * Rule descriptions can include other asciidoc files from the same rule
# directory or from shared_content.
# * All asciidoc files are used/included.
echo "Testing the following rules: ${affected_rules}"
supportedLanguages=$(sed 's/ or//' supported_languages.adoc | tr -d '`,')
for dir in $affected_rules
do
if [ ! -d "$dir" ]; then
echo "Apparently $dir is deleted, skipping"
continue
fi
dir=${dir%*/}
# check if there are language specializations
subdircount=$(find "$dir" -maxdepth 1 -mindepth 1 -type d | wc -l)
if [[ "$subdircount" -eq 0 ]]
then
# no specializations, that's fine if the rule is deprecated
if grep -Pq '"status": "(deprecated|closed)"' "$dir/metadata.json"; then
echo "INFO: deprecated generic rule $dir with no language specializations"
else
echo "ERROR: non-deprecated generic rule $dir with no language specializations"
exit_code=1
fi
else
# Add the full path of all adoc files that were affected for sanitization
find ~+/"${dir}" -name '*.adoc' >> all_asciidocs
for language in "${dir}"/*/
do
language=${language%*/}
if [[ ! "${supportedLanguages[*]}" == *"${language##*/}"* ]]; then
if [[ ! "${ALLOWED_RULE_SUB_FOLDERS[*]}" == *"${language##*/}"* ]]; then
echo "ERROR: ${language##*/} is not a supported language"
exit_code=1
fi
else
RULE="$language/rule.adoc"
if test -f "$RULE"; then
# Errors emitted by asciidoctor don't include the full path.
# https://github.com/asciidoctor/asciidoctor/issues/3414
# To ease debugging, we copy the rule.adoc into tmp_SXYZ_language.adoc
# and run asciidoctor on them instead.
# We add the implicit header "Description" to prevent an asciidoctor warning.
TMP_ADOC="$language/tmp_$(basename "${dir}")_${language##*/}.adoc"
echo "== Description" > "$TMP_ADOC"
cat "$RULE" >> "$TMP_ADOC"
else
echo "ERROR: no asciidoc file $RULE"
exit_code=1
fi
fi
done
fi
done
cd rspec-tools
cat ../all_asciidocs | xargs pipenv run rspec-tools check-asciidoc >validate_asciidoc 2>&1
if [ -s validate_asciidoc ]; then
echo "ERROR: Invalid asciidoc description:"
cat validate_asciidoc
exit_code=1
fi
rm -f validate_asciidoc ../all_asciidocs
cd ..
# Run asciidoctor and fail if a warning is emitted.
# Use the tmp_SXYZ_language.adoc files (see note above).
ADOC_COUNT=$(find rules -name "tmp*.adoc" | wc -l)
if (( ADOC_COUNT > 0 )); then
if asciidoctor --failure-level=WARNING -o /dev/null rules/*/*/tmp*.adoc; then
if asciidoctor -a rspecator-view --failure-level=WARNING -o /dev/null rules/*/*/tmp*.adoc; then
echo "${ADOC_COUNT} documents checked with success"
else
echo "ERROR: malformed asciidoc files in rspecator-view"
exit_code=1
fi
else
echo "ERROR: malformed asciidoc files"
exit_code=1
fi
else
echo "No new asciidoc file changed"
fi
find rules -name "tmp*.adoc" -delete
# Validate file inclusion, cross-references, and other properties.
#
# This part of the validation is extracted in a separate script,
# which is covered by tests unlike what is above this line.
TOPLEVEL=. ./ci/asciidoc_validation/validate.sh || exit_code=1
if (( exit_code == 0 )); then
echo "Success"
else
echo "There were errors"
fi
exit $exit_code

25
ci/validate_file_extensions.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
#
# Validates that there are no files with .cs, .vb, .razor or .cshtml extensions present inside rules folder.
#
# As part of the new DotNet squad rule specification sprint guidelines, test case files similar to the ones
# used for unit tests in sonar-dotnet should be temporarely added under the rule folder on RSPEC repository.
# Those files can be of any number for both C# (*.cs, *.razor, *.cshtml) and VB.NET (.*vb).
# The test case files will be copied to the sonar-dotnet repository during the initial phases of implementation
# and will serve as an initial test bed.
# Before merging the PR on the RSPEC side, it is important to ensure that these test case files are deleted.
# The script make sure to fail the CI if any of those previously mentioned files are present inside the rules folder.
set -euxo pipefail
TOPLEVEL="$(realpath .)"
RULES_DIR="${TOPLEVEL}/rules"
CSVB_FILES=($(find "${RULES_DIR}" -type f -name "*.cs" -o -name "*.vb" -o -name "*.razor" -o -name "*.cshtml"))
if [ ${#CSVB_FILES[@]} -gt 0 ]; then
echo "ERROR: '.cs','.vb','.razor' or '.cshtml' files are detected."
printf '%s\n' "${CSVB_FILES[@]}"
exit 1
else
echo "SUCCESS: no '.cs' or '.vb' files detected."
exit 0
fi

24
ci/validate_links.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -euxo pipefail
CACHE_PATH=$1
echo "CACHE_PATH: $CACHE_PATH"
[ ! -d $CACHE_PATH ] && mkdir $CACHE_PATH
ls -al $CACHE_PATH
[ -f "$CACHE_PATH/link_probes.history" ] && cp "$CACHE_PATH/link_probes.history" ./rspec-tools/
./ci/generate_html.sh
# validate the links in asciidoc
cd rspec-tools
if pipenv install && pipenv run rspec-tools check-links --d ../out ; then
EXIT_CODE=0
else
EXIT_CODE=1
fi
cd ..
cp ./rspec-tools/link_probes.history "$CACHE_PATH/"
exit $EXIT_CODE

33
ci/validate_metadata.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
set -ueo pipefail
shopt -s lastpipe # To pipe command result into mapfile and have the array variable available in the main shell process.
git fetch --quiet "${CIRRUS_DEFAULT_ORIGIN:-origin}" "${CIRRUS_DEFAULT_BRANCH:-master}"
base="$(git merge-base FETCH_HEAD HEAD)"
echo "Comparing against the merge-base: ${base}"
if ! git diff --name-only --exit-code "${base}" -- rspec-tools/
then
basename --multiple rules/* | mapfile -t affected_rules
echo "Change in the tools, revalidating all rules"
else
git diff --name-only "${base}" -- rules/ | # Get all the changes in rules
sed -Ee 's#(rules/S[0-9]+)/.*#\1#' | # extract the rule directories
sort -u | # deduplicate
while IFS= read -r rule; do if [[ -d "$rule" ]]; then echo "$rule"; fi done | # filter out deleted rules
sed 's#rules/##' | # get rule ids
mapfile -t affected_rules # store them in the `affected_rules` array
echo "Validating ${affected_rules[*]}"
fi
printf '\n\n\n'
# Validate metadata
if [[ "${#affected_rules[@]}" -gt 0 ]]
then
cd rspec-tools
pipenv install
printf '\n\n\n'
pipenv run rspec-tools validate-rules-metadata "${affected_rules[@]}"
else
echo "No rule changed or added"
fi

View File

@ -0,0 +1,99 @@
#!/usr/bin/env bash
#
# Run integration tests for ci/asciidoc_validation.
set -uo pipefail
# We could write complex checks to ensure only specific commands fail and emit
# a specific error message. Instead, we rely on `set -xe` to consistently and
# reliably exit with non-zero if any command fails and pinpoint which command
# failed in the trace output. We also use a trap on ERR to give users a short
# hint, and `set -E` to propagate this trap to shell functions and subshells.
#
# This allows use to write tests as simple commands, such as
# test -f file_exists
#
# When we want to ensure a command fails, we use this pattern:
# { ! command; }
set -xeE
err_trap() {
set +x # Disable tracing when displaying stackframe.
echo "Some test failed; look at the trace for more. Here is the stackframe:" >&2
i=0
while caller $i >&2
do
(( i++ )) || :
done
}
trap err_trap ERR
# Ensure the script we test exists and is executable.
GIT_TOPLEVEL_DIR="$(git rev-parse --show-toplevel)"
VALIDATE_SCRIPT="${GIT_TOPLEVEL_DIR}/ci/asciidoc_validation/validate.sh"
test -f "${VALIDATE_SCRIPT}"
test -x "${VALIDATE_SCRIPT}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
run_test() {
# Run validation script on $1.
# Ensure the output contains $2...$N.
tmp="$(mktemp -d)"
stderr_log="${tmp}/stderr_log"
stdout_log="${tmp}/stdout_log"
if TOPLEVEL="${SCRIPT_DIR}/$1" "${VALIDATE_SCRIPT}" 2> "${stderr_log}" > "${stdout_log}"
then
# The validation succeeded. We expect nothing in the output.
[ $# -eq 1 ] # no tests
test -f "${stderr_log}"
{ ! test -s "${stderr_log}"; }
else
# We expect at least on check of the stderr content.
[ $# -gt 1 ]
shift
for query in "$@"
do
test -n "${query}"
grep -q -e "${query}" "${stderr_log}"
done
fi
# Regardless of success or failure, the stdout is expected to be empty.
test -f "${stdout_log}"
{ ! test -s "${stdout_log}"; }
}
run_test "test_valid"
run_test "test_unused_adoc" \
"ERROR: These adoc files are not included anywhere:" \
"rules/S100/java/unused.adoc" \
"rules/S100/unused.adoc" \
"shared_content/unused.adoc"
run_test "test_bad_cross_ref" \
"ERROR: Some rules try to include content from unallowed directories." \
"S100 cross-references .*rules/S1000/bad.adoc" \
"S1000 cross-references .*rules/S100/java/bad.adoc"
run_test "test_diff_source" \
"ERROR: Diff highlighting is used incorrectly:" \
"\[S100/cfamily] diff-type is missing in .*/rules/S100/cfamily/rule.adoc:3" \
"\[S100/cfamily] diff-id is missing in .*/rules/S100/cfamily/rule.adoc:8" \
"\[S100/cfamily] diff-type 'bad' is not valid in .*/rules/S100/cfamily/rule.adoc:13" \
"\[S100/cfamily] diff-type is missing in .*/rules/S100/cfamily/local.adoc:3" \
"\[S100/cfamily] diff-id is missing in .*/rules/S100/cfamily/local.adoc:8" \
"\[S100/cfamily] diff-type 'local' is not valid in .*/rules/S100/cfamily/local.adoc:13" \
"\[S100/cfamily] diff-type is missing in .*/shared_content/cfamily/shared.adoc:3" \
"\[S100/cfamily] diff-id is missing in .*/shared_content/cfamily/shared.adoc:8" \
"\[S100/cfamily] diff-type 'shared' is not valid in .*/shared_content/cfamily/shared.adoc:13" \
"\[S100/java] diff-id=1 has zero noncompliant example: .*/rules/S100/java/rule.adoc:3" \
"\[S100/java] diff-id=2 has zero compliant example: .*/rules/S100/java/rule.adoc:8" \
"\[S100/java] diff-id=3 has zero noncompliant example: .*/shared_content/java/example.adoc:3" \
"\[S100/java] diff-id=4 has too many noncompliant examples: .*/rules/S100/java/rule.adoc:15, .*/shared_content/java/example.adoc:13, .*/shared_content/java/example.adoc:8" \
"\[S200/default] diff-id=1 has too many noncompliant examples: .*/rules/S200/rule.adoc:12, .*/rules/S200/rule.adoc:2, .*/rules/S200/rule.adoc:7" \
"\[S200/default] diff-id=2 has too many noncompliant examples: .*/rules/S200/rule.adoc:17, .*/rules/S200/rule.adoc:22" \
"\[S200/default] diff-id=2 has zero compliant example: .*/rules/S200/rule.adoc:17, .*/rules/S200/rule.adoc:22"
echo "All tests passed"

View File

@ -0,0 +1,3 @@
Bad include from S100
include::../../S1000/bad.adoc[]

View File

@ -0,0 +1 @@
java S100

View File

@ -0,0 +1,3 @@
This include is fine
include::./bad.adoc[]

View File

@ -0,0 +1 @@
File from S1000

View File

@ -0,0 +1 @@
include::../../../shared_content/java/shared.adoc[]

View File

@ -0,0 +1 @@
include::../../rules/S100/java/bad.adoc[]

View File

@ -0,0 +1,15 @@
[source,cpp,diff-id=1]
----
Missing diff-type
----
[source,cpp,diff-type=compliant]
----
Missing diff-id
----
[source,c,diff-id=1,diff-type=local]
----
Bad diff-type
----

View File

@ -0,0 +1,19 @@
[source,cpp,diff-id=1]
----
Missing diff-type
----
[source,cpp,diff-type=compliant]
----
Missing diff-id
----
[source,c,diff-id=1,diff-type=bad]
----
Bad diff-type
----
include::./local.adoc[]
include::../../../shared_content/cfamily/shared.adoc[]

View File

@ -0,0 +1,17 @@
[source,java,diff-id=1,diff-type=compliant]
----
1. compliant but missing noncompliant
----
[source,java,diff-id=2,diff-type=noncompliant]
----
2. noncompliant but missing compliant
----
include::../../../shared_content/java/example.adoc[]
[source,java,diff-id=4,diff-type=noncompliant]
----
4. noncompliant B
----

View File

@ -0,0 +1,24 @@
[source,diff-id=1,diff-type=compliant]
----
1. compliant
----
[source,diff-id=1,diff-type=noncompliant]
----
1. noncompliant A
----
[source,diff-id=1,diff-type=noncompliant]
----
1. noncompliant B
----
[source,diff-id=2,diff-type=noncompliant]
----
2. noncompliant A
----
[source,diff-id=2,diff-type=noncompliant]
----
2. noncompliant B
----

View File

@ -0,0 +1,15 @@
[source,cpp,diff-id=1]
----
Missing diff-type
----
[source,cpp,diff-type=compliant]
----
Missing diff-id
----
[source,c,diff-id=1,diff-type=shared]
----
Bad diff-type
----

View File

@ -0,0 +1,15 @@
[source,java,diff-id=3,diff-type=compliant]
----
3. compliant but missing noncompliant
----
[source,java,diff-id=4,diff-type=compliant]
----
4. compliant but too many noncompliant
----
[source,java,diff-id=4,diff-type=noncompliant]
----
4. noncompliant A
----

View File

@ -0,0 +1,3 @@
I am a main file.
include::./used.adoc[]

View File

@ -0,0 +1 @@
I am included.

View File

@ -0,0 +1,3 @@
I am a main file.
include::../../../shared_content/java/included.adoc[]

View File

@ -0,0 +1 @@
I got included.

View File

@ -0,0 +1 @@
another

View File

@ -0,0 +1,32 @@
S100 cfamily
include::./another.adoc[]
include::../../../shared_content/anything.adoc[]
[source,cpp,diff-id=1,diff-type=noncompliant]
----
noncompliant
----
[source,cpp,diff-id=1,diff-type=compliant]
----
compliant
----
[source,cpp,diff-id=2,diff-type=noncompliant]
----
noncompliant
----
[source,cpp,diff-id=2,diff-type=compliant]
----
compliant A
----
[source,cpp,diff-id=2,diff-type=compliant]
----
compliant B
----

View File

@ -0,0 +1,3 @@
generic description
include::../../shared_content/anything.adoc[]

View File

@ -0,0 +1,3 @@
S200 java
include::../../../shared_content/anything.adoc[]

View File

@ -0,0 +1 @@
anything

View File

@ -0,0 +1,24 @@
= AsciiDoc Dos & Don'ts
=== Include
Make sure that `include` statements are surrounded by blank lines.
AsciiDoc will sometimes (depending on the version) trim whitespaces at the beginning and end of the included files. Any adjacent text will thus be stuck to the inlined content, which could lead to display issues like swallowed title tags.
==== Write
----
== Title
include::how-to-fix-it/core.adoc[]
include::how-to-fix-it/symfony.adoc[]
----
==== Avoid
----
== Title
include::how-to-fix-it/core.adoc[]
include::how-to-fix-it/symfony.adoc[]
----

51
docs/benchmarks.adoc Normal file
View File

@ -0,0 +1,51 @@
= Benchmarks
When writing a rule that has to do with performance, you might need to showcase some benchmarks as proof of potential improvements. This should be included in a separate section called `Benchmarks`, under `Resources`.
The format of the benchmarks section is as follows:
* Benchmark table
* Benchmarking code
* Hardware configuration
== Benchmark table
- Do not use abbreviations for statistical terms, not everyone is familiar with them.
- In the `Benchmarks` section, add a subsection called `Glossary` right after the benchmark table and add links to Wikipedia to explain statistical terms used in the header of each column:
==== Glossary
* https://en.wikipedia.org/wiki/Arithmetic_mean[Mean]
* https://en.wikipedia.org/wiki/Standard_deviation[Standard Deviation]
* https://en.wikipedia.org/wiki/Memory_management[Allocated]
Ideally, the columns should look like in the following example:
|===
| <What is being measured> | Mean | Standard Deviation | Allocated
| <This> | 5.042 ms | 0.1049 ms | 125 KB
| <That> | 2.691 ms | 0.0334 ms | 85.94 KB
|===
== Benchmarking code
The code that was used to generate the benchmarks should be included to provide transparency and allow others to reproduce and verify the results. Preferably, the code snippet should include the sample size, the number of iterations, and the framework/library used to run the benchmarks.
If the code is not showcasing how the results were generated, consider prefixing it with an explanation that links to the framework/library used, for example:
----
The results were generated by running the following snippet with https://github.com/dotnet/BenchmarkDotNet[BenchmarkDotNet].
----
== Hardware configuration
The hardware configuration used to run the benchmarks should be included, for example:
[source]
----
BenchmarkDotNet=v0.13.5, OS=Windows 10 (10.0.19045.2846/22H2/2022Update)
11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
[Host] : .NET Framework 4.8 (4.8.4614.0), X64 RyuJIT VectorSize=256
.NET 7.0 : .NET 7.0.5 (7.0.523.17405), X64 RyuJIT AVX2
.NET Framework 4.6.2 : .NET Framework 4.8 (4.8.4614.0), X64 RyuJIT VectorSize=256
----

21
docs/deprecation.adoc Normal file
View File

@ -0,0 +1,21 @@
= Deprecation
== In the RSPEC
The rule status (`/status` in the metadata) should be set to `deprecated`, and its tags should be removed. If the rule is in `SonarWay`, it should be removed from this profile.
Optionally, `/extra/replacementRules` can list the rules that replace this rule.
The `superseded` status from Jira has been superseded by the `deprecated` status during transition to RSPEC V2.
== When running ruleAPI
We detect if replacement rules are specified and if they have been implemented or not.
* If they don't exist, the rule is exported as `deprecated`, and a text is added to the description
* If they are specified, but have not been implemented, the rule is not considered as deprecated (its status is set to `ready` during the export).
* If they are specified and implemented, the status remains `deprecated`, and a text mentioning the replacement rules is added to the description.
In all cases, the `/extra/replacementRules` is removed from the metadata available to the analyzer.

378
docs/description.adoc Normal file
View File

@ -0,0 +1,378 @@
= Rule Description
:toc:
This document describes how `+rule.adoc+` and its dependencies should be structured.
See also the <<styling_guide.adoc#,Styling Guidelines>>, <<tone_guide.adoc#,Tone Guide>> and https://docs.sonarqube.org/latest/extension-guide/adding-coding-rules/#coding-rule-guidelines[Coding rule guidelines].
For technical support, see also
* Rule <<../rules/S6620/java/rule.adoc#,S6620>>, which showcases asciidoc features that are available when writing a rule description.
* Rule <<../rules/S6778/java/rule.adoc#,S6778>>, which showcases multi-framework rules.
== Sections
There should be no first level titles (`+= Title+`) in your adoc.
The allowed second level titles and lower are described in their respective sections below, for each type of rule description.
== Types of rule description
There are currently 2 types of rule descriptions, each having a specific structure.
=== 1. Hotspot
The possible sections for this format are the following:
. Description (no title)
. Ask Yourself Whether
. Recommended Secure Coding Practices
. Sensitive Code Example
. Compliant Solution
. Exceptions
. See
. See Also
Third-level and fourth-level titles (`+=== Title t3+` and `+==== Title t4+`) are not checked for this type of rule.
=== 2. Learn as You Code Rule Format
This format is defined as follows:
* Short description (no title)
// This needs to be kept in sync with the [maps in the validation script](https://github.com/SonarSource/rspec/blob/master/rspec-tools/rspec_tools/validation/description.py#L32-L39).
* Why is this an issue? (mandatory)
** What is the potential impact?
** Exceptions
** (any other titles)
* How to fix it
** Code examples
*** Noncompliant code example
*** Compliant solution
** How does this work?
** Pitfalls
** Going the extra mile
** (any other titles)
* How to fix it in {Framework Display Name}
** Code examples
*** Noncompliant code example
*** Compliant solution
** How does this work?
** Pitfalls
** Going the extra mile
** (any other titles)
* Resources
** Documentation
** Articles & blog posts
** Conference presentations
** Standards
** External coding guidelines
** Benchmarks
** Related rules
Where the `How to fix it in {Framework Display Name}` section can be repeated multiple times when the rule description is defined per-framework.
The sections and subsections for this format are defined as follows:
* Short description (no title) [Optional]
+
A short introduction/summary of the topic, only a few sentences. +
Goal: The title (or message) of a rule might not always be clear due to its shortness, and this should make it clear to our users what the issue is about. Experienced developers will immediately understand what it is about, and developers not yet familiar with the issue at hand can dig deeper into the `Why is this an issue?` section.
+
* *Why is this an issue?* (level 2 title) *[Mandatory]*
+
Start at the basics and go into more depth to explain the concepts behind this type of issue. This is most likely the place where a lot of the content will be. +
This is the place to tell the “story” of the rule, including the impact of leaving it unfixed. We should include code samples wherever needed to make it easier to understand
what is going on. This can be in the form of noncompliant and compliant code in a single code box (noncompliant lines should always be highlighted with the corresponding comment
“// Noncompliant” optionally followed by some explanation) if that is clearer. This first tab could use a freeform 'story-telling' style explaining what the rule is
detecting and why. Feel free to use the “What is the potential impact?” title if it makes sense, or any other titles you find useful. +
Goal: Understand the concepts behind an issue and why it matters.
+
** *What is the potential impact?* (level 3 title) [Optional]
+
This subsection aims to address questions such as the following: +
What is the risk for me? What is the risk for my organization? What is the risk for the users? What can go wrong? +
This section aims to talk about the impacts on the software, depending on the code qualities this issue is linked to: security, reliability, or maintainability. **(see <<metadata.adoc#code-field,Impacts>>.)**
+
Consequently, this section can also talk about the impacts on the ecosystem of this software. For example, impacts on the organization, on the users, and impact in terms of regulations.
https://github.com/SonarSource/rspec/blob/a51217c6d91abfa5e1d77d0ae0843e3903adf2d0/rules/S3649/impact.adoc[_Example._] +
Goal: Our users should understand the impact of this issue on their project.
+
* *How to fix it* or *How to fix it in `{Framework Display Name}`* (level 2 title) [Optional; the title depends on whether the description is generic or framework-specifc. See examples below.]
+
This section consists of one or multiple fixes for this type of issue (`Noncompliant code example` vs. `Compliant solution`). There can be multiple fixes for different libraries and/or frameworks.
If the fix for the rule is trivial (quickfix is available, it is easily inferred from the title and/or message), this section should be omitted and the fix could be mentioned in the previous section.
This tab could also use a freeform 'story-telling' style if that makes it clearer for the user. Feel free to use any of the titles below, or any other titles you find useful. +
Goal: Get an idea of how this issue can be fixed for my project/framework, why this works, what to look out for, and also how to continue improving on this topic.
+
** *How does this work?* (level 3 title) [Optional]
+
Explain why this fixes the problem.
+
** *Pitfalls* (level 3 title) [Optional]
+
One or multiple pitfalls to take into account when working on fixing this issue.
https://github.com/SonarSource/rspec/blob/a51217c6d91abfa5e1d77d0ae0843e3903adf2d0/rules/S6096/common/pitfalls/partial-path-traversal.adoc[_Example._]
+
** *Going the extra mile* (level 3 title) [Optional]
+
Even though the issue might be fixed, most of the time there can be way/s to further improve on this issue or to harden your project.
The subsection should be concise.
https://github.com/SonarSource/rspec/blob/a51217c6d91abfa5e1d77d0ae0843e3903adf2d0/rules/S5131/common/extra-mile/csp.adoc[_Example._]
+
* *Resources* (level 2 title) [Optional]
+
Include resources if our users want to dig even deeper, that can be presented in the different categories.
https://github.com/SonarSource/rspec/tree/a51217c6d91abfa5e1d77d0ae0843e3903adf2d0/rules/S5131/common/resources[_Example._] +
Goal: Allow the user to dig deeper by providing a curated list of resources.
+
** *Documentation* (level 3 title) [Optional]
** *Articles & blog posts* (level 3 title) [Optional]
** *Conference presentations* (level 3 title) [Optional]
** *Standards* (level 3 title) [Optional]
** *External coding guidelines* (level 3 title) [Optional]
** *Benchmarks* (level 3 title) [Optional]
** *Related rules* (level 3 title) [Optional]
+
This section lists Sonar rules related to the current one. The rule ID(s) should be followed by the rule title(s) or a sentence explaining the relation between the rules, e.g.: "_S2275 and S3457 specialize in detecting type mismatches with format strings._".
+
xref:link_formatting.adoc[Standard for links is defined in this document.]
Content of the section "_How to fix it_ / _How to fix it in {Framework Display Name}_" can either be generic or framework specific.
When the content is generic, the "_How to fix it_" title must be used, and the section should only appear once. Example:
....
== Why is this an issue?
Explanation of why this is bad.
== How to fix it
=== Code examples
==== Noncompliant code example
[source,js,diff-id=1,diff-type=noncompliant]
----
var myExample;
----
==== Compliant solution
[source,js,diff-id=1,diff-type=compliant]
----
var myExample = 0;
----
=== How does this work?
We added something.
== Resources
=== Documentation
http-address-of-documentation[My doc name]
....
Note that you can see two special attributes (`diff-id` and `diff-type`) used in the code examples above, these attributes are explained in the <<Diff view,Diff view>>
section below.
When the content is framework-specific, one or more "_How to fix it in `{Framework Display Name}`_" sections (with their respective subsections) must be present.
Each repetition will represent the specific _How to fix it_ section of a given framework.
For example:
....
== How to fix it in Spring
=== Code examples
... Some generic text and code examples for Spring...
=== How does this work?
... Explanation about how the exploit works in Spring...
=== Pitfalls
... Generic and Spring-specific pitfalls to avoid when fixing the issue...
== How to fix it in JSP
=== Code examples
... Some generic text and code examples for JSP...
=== How does this work?
... Explanation about how the exploit works in JSP...
=== Pitfalls
... Generic and JSP-specific pitfalls to avoid when fixing the issue...
....
Ideally, by convention and for maintainability, each framework _How to fix it_ section will be defined in separate files.
Ex:
....
== Why is this an issue?
... Explanation ...
# How to fix it sections
include::./how-to-fix-it/framework-1.adoc[]
include::./how-to-fix-it/framework-2.adoc[]
== Resources
=== Documentation
http-address-of-documentation[My doc name]
....
Note that each framework-specific _How to fix it_ subsection must start with an H2 title following the given format:
`== How to fix it in [an|a|the]? {Framework name}`.
This is important, as this format will be expected by the analyzers when loading the rule content to recognize the different subsections.
Furthermore, the display name of the framework has to match an allowed framework
display name, as defined in <<header_names/allowed_framework_names.adoc#,this allowed framework names file>>.
==== General guidance
Most sections and subsections of the Learn as You Code rule format are optional, only the `Why is this an issue?` main section is mandatory.
The goal is to provide the right level of guidance so that users get the right information from the rules at the right time.
Do not feel obliged to use every section or sub-section if omitting them would lead to a better user experience.
==== Guidelines if you arent sure where something belongs in a rule
* If you need the information to fix the issue, but only the first time, it probably belongs in Why is this an issue?
* If you need the information to fix the issue every time, it probably belongs in How to fix it?
* If you dont need the information to fix the issue, but it will help users grow their knowledge, it probably belongs in Resources (this maps to the 'More Info' tab in the products)
By being careful about what goes where, we help to ensure that users get exactly what they need, when they need it.
==== Guidelines on content focus
We want to help users to create Clean Code. Rule content should focus on the manner in which the code is not clean, why this is an issue, and how to remedy this.
Rules should talk about the potential impact on software quality in the 'What is the potential impact?' sub-section.
For example, if you are talking about a locking issue, it makes sense to focus on the logical issues that could lead to a deadlock and how to fix that in the main rule content.
The implications of a deadlock on the application reliability would then go into the 'What is the potential impact?' sub-section.
== Code Examples
Whenever possible, prefix your code blocks with `[source,language]`, in order to get syntax coloring.
....
[source,cpp]
----
int main(int argc, const char** argv) {
return 0;
}
----
....
That is mandatory for the Noncompliant and Compliant code example sections, just recommended - at the moment - for other sections.
The language names accepted are usually the name we already use for the language folders in RSPEC. Exceptions are:
cfamily:: use `cpp`, `c`, or `objectivec`
plsql:: use `sql`
tsql:: use `sql`
In case no language is appropriate for a code block (for example shared examples between multiple languages), you can use `text` as the language.
=== Comments within code blocks
Colon (`:`) should be used as separator between `Noncompliant`/`Compliant` comments and the text explanation that follows, if any.
[source,cpp]
----
int X = 2; // Noncompliant: variable should be in lowercase
----
When referencing a name within a comment in a code example, use double quotes to make it clear it refers to an existing element in the code.
[source,cpp]
----
int i = 0;
cout << noexcept(++i); // Noncompliant: "i" is not incremented
----
=== Diff view
Additionally, you can also use two attributes to let the products know your code examples should be highlighted with a diff view when possible
(showing the changes in the code examples as red/green).
These attributes are optional and if a product does not yet support the diff view feature, these attributes will simply be ignored.
These attributes are `diff-id=X` and `diff-type=[noncompliant|compliant]`. The `diff-id` attributes describe which code examples should
be compared together, and the `diff-type` attribute explain how it should be displayed `Noncompliant` (red) vs. `Compliant` (green).
A single and unique diff-id should be used only once for each type of code example as shown in the description of a rule.
....
==== Noncompliant code example
[source,js,diff-id=1,diff-type=noncompliant]
----
var myExample;
----
==== Compliant solution
[source,js,diff-id=1,diff-type=compliant]
----
var myExample = 0;
----
....
== Parameters
Parameters should be listed in a subsection as follow:
....
=== Parameters
.name
****
_TYPE_
----
default value
----
Description of what the parameter does.
****
.name2
****
----
another default value
----
Description of what this second parameter does.
****
.name3
****
_TYPE_
Description of what this third parameter does.
****
.name4
****
Description of what this fourth parameter does.
****
....
The parameter name and the description are mandatory. The type and default value are not.
The parameter name with a `.` before will be the title of the block below marked by `****`.
We always use `----` around the default parameter to avoid having a special character confuse AsciiDoctor and to create a visual consistency for all parameters.
== Comment a rule
Comments and links that were created on Jira have been gathered in a `comments-and-links.adoc` file for each concerned rule. +
You can add a comment anywhere in a rule by adding the following lines in the `*.adoc` file:
[source]
----
\ifdef::env-github,rspecator-view[]
John Doe (9 Jun 2021, 15:49): my comment on the rule
\endif::env-github,rspecator-view[]
----
This way, your comment will only be visible in GitHub preview and on the Search Page (and will not be visible for the user).
== Share content between rules
You can share content between rules by using the `shared_content` folder at the root of the repository.
Any included content for a rule can only come from the folder of the rule being described, `shared_content`, or any of their subfolders.

View File

@ -0,0 +1,165 @@
// Ansible
// C#
* ASP.NET
* ASP.NET Core
* ASP.NET MVC 4.x
* Razor
* .NET
* Entity Framework Core
* Dapper
* BouncyCastle
* Jwt.Net
* Blazor
// C-Family
* Botan
* CryptoPP
* OpenSSL
* cURL
* MSTest
* NUnit
* xUnit
* Fluent Assertions
* NFluent
* NSubstitute
* MSTest
* NUnit
* Xerces
* libxml2
// Java
* Android
* Android WebView
* Apache Commons
* Apache Commons
* Apache Commons Email
* Apache HttpClient
* Auth0 JWT
* Commons Compiler
* Dom4j
* FasterXML
* Groovy
* Gson
* Hibernate
* Java Cryptography Extension
* Java EE
* Java JWT
* Java SE
* Java JDBC API
* Java I/O API
* Jdom2
* JSP
* Legacy Mongo Java API
* OkHttp
* Realm
* Apache HttpClient
* Couchbase
* SAX
* Servlet
* Spring
* Spring Data MongoDB
* Spring Data Cassandra
* Spring Data Redis
* Spring Data Neo4j
* SQLCipher
* Thymeleaf
// JS
* Jasmine
* Jest
* Flow.js
* Node.js
* Express.js
* SSH2
* Mocha
* MongoDB
* Mongoose
* Sequelize
* Knex
* DOM API
* jsonwebtoken
* libxmljs
* Formidable
* Multer
* Passport
* Request
* TypeScript
* PropTypes
* JSX
* Electron
// PHP
* Core PHP
* Guzzle
* Laminas
* Laravel
* Symfony
* WordPress
* Mcrypt
// Python
* aiohttp
* Amazon DynamoDB
* Argon2-cffi
* Bcrypt
* Cryptodome
* databases
* Django
* Django Templates
* FastAPI
* Flask
* HTTPX
* Jinja
* lxml
* MySQL Connector/Python
* Numpy
* Paramiko
* pyca
* PyCrypto
* pyDes
* PyJWT
* pyOpenSSL
* python-jose
* python-jwt
* python-ldap
* Python SQLite
* Python Standard Library
* PyTorch
* PyYAML
* Requests
* Scrypt
* Scikit-Learn
* SignXML
* SQLAlchemy
* ssl
* TensorFlow
// Docker
* Wget
// Cloudformation
* API Gateway
* OpenSearch
* Identity and Access Management
// Azure Resource Manager
* Storage Accounts
* Databases
* ARM Templates
* Bicep
// Terraform
* AWS API Gateway
* AWS OpenSearch
* Azure Databases
* Azure Storage Accounts
* GCP Load Balancers
* AWS Identity and Access Management
// CDK
* AWS CDK
// Swift
* CommonCrypto
* CryptoSwift
* IDZSwiftCommonCrypto
// Azure resource manager
* JSON templates
* Bicep
// PL/SQL
* DBMS_CRYPTO
// Go
* Go Standard Library
// Kubernetes
* Helm
// Kotlin
Jetpack Compose

View File

@ -0,0 +1,6 @@
* Ask Yourself Whether
* Sensitive Code Example
* Recommended Secure Coding Practices
* Compliant Solution
* Exceptions
* See

83
docs/link_formatting.adoc Normal file
View File

@ -0,0 +1,83 @@
== Why is it important to define a standard for links?
We often want to provide links in the 'Resources' section of a rule that point to blog posts, documentation, and other rules. To provide a consistent experience across the Sonar solution, we want to define standard ways to write these links in our rules. This document aims to do that. This guidance does not apply to links that appear within continuous text elsewhere in a rule.
== How should you write links?
=== Link formatting:
* Links to documentation: <source> - <title> e.g. SonarQube Documentation - SonarScanner for Gradle
* Links to framework documentation <source> - <member name and kind> e.g. Microsoft Learn - Object.ToString Method
* Links to blog posts / news articles: <source> - <title> e.g. Google Security Blog - Moving towards a more secure web
* Links to Stack Overflow answers (similar comments in a blog)e.g. Stack Overflow - Answer by Stephen Cleary for Best way to handle null task inside async method?
* Links to another Sonar rule: <rulenumber> - <title> e.g. S2755 - XML parsers should not be vulnerable to XXE attacks.
Note that rule-ids (`<rulenumber>`) are automatically hyperlinked in our products to point to the rule description in that product.
_Do not add any hyperlink yourself._
The hyperlink to anything apart from other Sonar rules should be applied to just the document name, with the 'source' being left as plain text. The idea is that this makes it really easy for a user to understand the source before they click on anything.
For Sonar rules, the whole entry (<rulenumber> - <title>) should be a hyperlink.
Here is how the above links should look like:
* SonarQube Documentation - https://docs.sonarqube.org/9.7/analyzing-source-code/scanners/sonarscanner-for-gradle/[SonarScanner for Gradle]
* Microsoft Learn - https://learn.microsoft.com/en-us/dotnet/api/system.object.tostring[`Object.ToString` Method]
* Google Security Blog - https://security.googleblog.com/2016/09/moving-towards-more-secure-web.html[Moving towards a more secure web]
* Stack Overflow - Answer by Stephen Cleary for https://stackoverflow.com/a/27551261[Best way to handle null task inside async method?]
* S2755 - XML parsers should not be vulnerable to XXE attacks +
_Note, no link here, this is the intended behavior. In the products the "S2755" part will be automatically hyperlinked._
=== Additional things to consider when adding a link to a rule:
* Is the article (particularly blog posts) likely to be around for at least 6 months?
* Do you trust this source as an 'expert'?
* Link to en_US versions where there is a choice
=== Short form names for sources we regularly use
When web pages have massively long names like "Java™ Platform, Standard Edition 8 API Specification", we will provide short form names that we will use instead. Feel free to add new ones below:
==== Short form names to use:
* Android Documentation - https://developer.android.com/docs
* Apex Developers Guide - https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_keywords_sharing.htm
* AWS Documentation - https://docs.aws.amazon.com/
* AWS blog - https://aws.amazon.com/blogs
* Azure Documentation - https://learn.microsoft.com/en-us/azure/?product=popular
* CERT - https://wiki.sei.cmu.edu/confluence/display/seccode
* Clippy Lints - https://rust-lang.github.io/rust-clippy/master/index.html
* {cpp} reference - https://en.cppreference.com/w/
* {cpp} Core Guidelines - https://github.com/isocpp/CppCoreGuidelines/blob/e49158a/CppCoreGuidelines.md
* CVE - https://cve.mitre.org
* CWE - https://cwe.mitre.org
* Docker Documentation - https://docs.docker.com/
* Google Cloud - https://cloud.google.com/docs
* Java Documentation - https://docs.oracle.com/en/java/
* Kotlin Documentation - https://kotlinlang.org/docs/home.html
* Kubernetes Documentation - https://kubernetes.io/docs/home/
* Microsoft Learn - https://learn.microsoft.com/en-us/
* Microsoft Developer Blog - https://devblogs.microsoft.com/
* MDN web docs - https://developer.mozilla.org/en-US/
* Medium - https://medium.com/
* MITRE - https://attack.mitre.org/
* Mockito - https://site.mockito.org/javadoc/current/overview-summary.html
* The Open Group - https://www.opengroup.org/
* OWASP - https://owasp.org/
* PEP - https://peps.python.org/
* PHP Documentation - https://www.php.net/docs.php
* PHP Tutorials - https://www.phptutorial.net/
* PEP 8 - Style Guide for Python Code - https://peps.python.org/pep-0008/
* Python Documentation - https://docs.python.org/
* React Documentation - https://reactjs.org/
* Rhino Security Labs - https://rhinosecuritylabs.com/
* SAP Documentation - http://help.sap.com/abapdocu_702/en/abenabap.htm
* SonarQube Documentation - https://docs.sonarqube.org/latest/
* Sonar - https://www.sonarsource.com/
* Sonar Blog - https://www.sonarsource.com/blog/
* Stack Overflow - https://stackoverflow.com/
* Symfony - https://symfony.com/doc/current/index.html
* Test NG Documentation - https://testng.org/doc/documentation-main.html
* W3Schools - https://www.w3schools.com/
* WCAG - https://www.w3.org/WAI/standards-guidelines/wcag/
* Wikipedia - https://en.wikipedia.org

45
docs/metadata.adoc Normal file
View File

@ -0,0 +1,45 @@
ifdef::env-github[]
:tip-caption: :bulb:
:note-caption: :information_source:
endif::[]
= Rule Metadata
This document describes how `+metadata.json+` should be structured.
== `title`, `type`, `tags`, `remediation`, and `defaultSeverity`
These fields are described in the https://docs.sonarqube.org/latest/extension-guide/adding-coding-rules/#coding-rule-guidelines[SonarQube documentation].
== `quickfix` field
Every active rule that is not a security hotspot must specify the availability of a quick fix for its issues.
`metadata.json` must feature a `quickfix` field with one of the following values:
* `unknown`: the feasibility of producing a quick fix is not evaluated.
* `infeasible`: it is not feasible to propose a quick fix for any of the issues, for whatever reason.
* `targeted`: it is possible to implement quick fixes for this rule, but none are implemented right now.
* `partial`: some of the issues produced by the rule propose a quick fix, but not all.
* `covered`: all the issues produced by the rule propose a quick fix.
[NOTE]
====
A `covered` rule is still not guaranteed to provide a quick fix for a particular issue (for instance, if a fix location would be inside a macro expansion or in a different file from the issue location). The rule is said to have a `partial` quick fix only if there exists a family of issues that cannot have a quick fix.
For instance, on one hand, if a rule detects two functions that are dangerous to use, `A` and `B`, and `A` has an obvious replacement (and therefore a quick fix) while `B` does not, the field should be set to `partial`.
On the other hand, if a quick fix could be easily proposed for both `A` and `B`, but the fix location might be inside a macro expansion, or in a different file from the issue location (and hence not feasible given the current SonarLint capabilities), this should not prevent the rule from being tagged as `covered`.
====
[TIP]
====
You can update the quickfix field using this GitHub Workflow: https://github.com/SonarSource/rspec/actions/workflows/update_quickfix_status.yml
====
== `code` field
The code field is an object that contains information related to the clean code taxonomy. It is an object with two required properties:
* `impacts`: A nested object that is treated as a mapping from a software quality to a level (`"INFO"`, `"LOW"`, `"MEDIUM"`, `"HIGH"` or `"BLOCKER"`). Note that at least one software quality has to be specified. The current list of allowed software qualities is `"MAINTAINABILITY"`, `"RELIABILITY"` and `"SECURITY"`.
* `attribute`: A single clean code attribute that the rule aims to achieve. This has to be one of the following values: `"FORMATTED"`, `"CONVENTIONAL"`, `"IDENTIFIABLE"`, `"CLEAR"`, `"LOGICAL"`, `"COMPLETE"`, `"EFFICIENT"`, `"FOCUSED"`, `"DISTINCT"`, `"MODULAR"`, `"TESTED"`, `"LAWFUL"`, `"TRUSTWORTHY"`, `"RESPECTFUL"`.

125
docs/styling_guide.adoc Normal file
View File

@ -0,0 +1,125 @@
= Styling Guide
This document provides styling guidelines for `+rule.adoc+` and its dependencies.
See also the <<description.adoc#,Description Guidelines>> for information about the rule structure and the <<tone_guide.adoc#,Tone Guide>> for guidance on the tone of voice to use.
The RSPEC styling guide is loosely based on the Associated Press Style and geared to rule descriptions.
The official Associated Press Style should be used as a fall-back for topics not defined here.
The guide might be extended in the future when unanimities emerge.
Following it should be considered a goal to work towards to get more homogenous texts.
This will be a long process consisting of many baby steps, such as adjusting the style when rule descriptions are rewritten for the Progressive Education Framework.
It is acceptable not to have 100% consistency across all texts. The higher the consistency, the better, but differences are expected.
== Language
Use _American English_ and its notation.
== Acronyms
Do not use acronyms without defining them first unless they are considered well known by the target audience.
For example, the acronym _JDK_ can be considered common knowledge for a Java developer and does not have to be defined.
The definition of what is and is not a well-known acronym is somewhat subjective.
It is up to the reviewers of RSPEC pull requests to challenge the use of acronyms that might not be well known.
If possible and sensible, spell out the acronym on first use and use generic terms on subsequent mentions.
For example, refer to _Cross-Site Scripting_ as _the vulnerability_.
Write:: Cross-Site Scripting allows to inject JavaScript code in the context of a website. The vulnerability can be abused to hijack sessions.
Avoid:: XSS allows to inject JS code in the context of a website. XSS can be abused to hijack sessions.
== Contractions
Contractions are considered informal writing and therefore should not be used.
Write:: It is the right way!
Avoid:: It's not the right way!
== Numbers
In general, spell out numbers one through nine. Use digits for numbers 10 and higher.
Be aware that there are many exceptions to this rule. For instance, use digits for units or percentages.
Write:: 5°C is 9% warmer than yesterday. The condition checks if the value is greater than eight and smaller than 100.
Avoid:: The condition checks if the value is greater than 8 and smaller than one hundred.
Commas should be used to group three digits of numbers larger than 999.
Write:: My program creates 1,000,000 forks.
Avoid:: My program creates 1000000 forks.
== Punctuation
=== Colon
Do not capitalize the first word after a colon unless it is the start of a sentence or a proper noun.
Write:: There is only one thing we can do: rewrite.
Avoid:: There is only one thing we can do: Rewrite.
=== Comma
To avoid ambiguity, add the Oxford comma after the penultimate term in a series of three or more terms.
Write:: My code is slick and works. My code is not slow, unreliable, and full of bugs.
Avoid:: My code is slick, and works. My code is not slow, unreliable and full of bugs.
=== Parentheses
Parentheses should be avoided. If the information is relevant, it is preferred to incorporate it directly in a sentence.
Write:: This is a test that forms an example.
Avoid:: This is a test (example).
== Lists
Do not capitalize the first word of list entries unless it is the start of a sentence or a proper noun.
Write::
Check the values:
* size
* length
Avoid::
Check the values:
* Size
* Length
Do not add punctuation for enumerations.
Write::
Check the values:
* size
* length
Avoid::
Check the values:
* size,
* length.
== Literals
Inline literals (backticks) should be used to highlight short values.
Use it when referencing variable names, file names, tokens, and all kinds of specific strings of text that should be visually extracted from the surrounding default text.
Write:: Compiling source file `src/generic_file.py` breaks an `assert` call in pytest framework.
Avoid:: Compiling source file "src/generic_file.py" breaks an `assert` call in `pytest` framework.
== Referencing elements from the code
When referencing elements from the code within a normal sentence, use the `backticks` (```) to format it. This includes variable names, function names, class names, and so on.
When referencing the same elements within a comment in a code block, surrpond it with double quotes.
[source,cpp]
----
int i = 0;
// Write
cout << noexcept(++i); // Noncompliant, "i" is not incremented -> Double quotes
// Avoid
cout << noexcept(++i); // Noncompliant, i is not incremented -> No quotes
cout << noexcept(++i); // Noncompliant, `i` is not incremented -> Backticks
----

66
docs/testing.adoc Normal file
View File

@ -0,0 +1,66 @@
= Testing changes
This document explains how to test changes to the frontend, CI, and rspec-tools.
Refer to the <<../README.adoc#AddModifyRule,main documentation>> if you want to modify rules.
== Testing the frontend
If your changes are small and noncontroversial, you can directly create a branch in this repository.
However, when you need to show reviewers how the website will look with your changes,
you can use a fork with your own GitHub Pages.
See <<forking>> for more details.
To test your changes locally, you can start a local HTTP server, such as ``npm start``.
Refer to the frontend documentation for more details.
== Testing the rspec-tools
Modifications to the rspec-tools do not require any special process.
You can therefore use a regular PR from a branch in this repository.
== Testing GitHub Actions
There are two strategies to test GitHub Actions: either use your own branch in this repository, or create a fork.
Choose the solution your need based on the impact your changes can have.
For example, if the changes may spam/modify other Pull Requests, it is wiser to use your own fork.
Note that GitHub will pick the workflow definitions from your branch whether it is in a fork or not.
It will also run new workflow scripts automatically (i.e. there is no configuration to change on GitHub).
When working on a new workflow or updating existing ones, since it may confuse other people to see unexpected results,
it is preferable to use your own fork while iterating on your work.
See <<forking>> for more details.
== Testing Cirrus CI
To test modification to the Cirrus CI script, you need to create your own branch in this repository (not a fork).
Cirrus CI will automatically pick up the version of the scripts on that branch.
[[forking]]
== Forking
Forking this repository is fairly trivial.
. Click "Fork" on the top right and select your own account.
. If needed, enable GitHub Actions:
.. in your fork, go to the "Actions" page,
.. click on the green button to enable workflows,
.. and, if needed, additionally enable the "Update rule coverage" workflow,
which is disabled by default because it executes on a schedule.
. If needed, enable GitHub Pages:
.. enable the GitHub Actions so that the ``gh-pages`` branch is properly populated,
.. go to "Settings" > "Pages" and under "Source"
*** set "Branch" to "gh-pages",
*** select "/ (root)" as the source folder,
*** and click "save".
If you need to rely on GitHub Actions, you can work on your fork's ``master`` branch to ensure that
the "Build and Deploy" workflow gets triggered out of the box.
Once you are done with your feature and your PR was merged, you can delete your fork.
If you prefer to keep the fork alive, don't forget to merge the upstream changes before working on your next feature.

37
docs/tone_guide.adoc Normal file
View File

@ -0,0 +1,37 @@
= Tone Guide
When writing a rule, we must consider who we are talking to, what they are trying to achieve, and what their mindset is likely to be.
== Who are we talking to?
We will be talking to a range of people; experienced engineers fixing something for the 10th time, people very new to the language, working on code they don't know well, and everyone in between. It is also important to realize that many of our users are not native English speakers.
What might they be doing, and how do they feel?
* They want to merge a PR and have a failing quality gate
** They may be tired, stressed, embarrassed, or under pressure to finish
*** Now is the time for a calm, well-informed mentor or coach, not for criticism, fear, or excessive humor
* They just saw they have a potential security hole
** They may be worrying about the implications for their job or getting a reprimand from a stern boss or security owner. They may be thinking this is yet another false positive and hovering over the wont fix button.
*** We need to be calm and factual. There is no need to scare them unduly and a friendly tone will definitely be helpful.
* They got that green quality gate, are basking in their success, and considering what to do next
** How can we help them turn that cheerful glow into motivation to learn more?
== Guidelines
*Tone:* We are your coding mentor. We are friendly and trustworthy but reasonably serious; we know what we are talking about, and we want to help you make the right change and grow in the process, at the right time for you.
* Aim for brevity and clarity in the 'Why is this an issue' and 'How to fix it' sections.
** Aim for just enough to make it clear what the key points are and no more. You can always add interesting stuff to the 'more info' section for when they have some spare time and want to learn more.
** Try to use simple English and avoid slang. Assume users have high school-level English, equivalent to CEFR levels B1/B2, at best. How easy can we make it to understand complex ideas?
* Write in the active voice rather than the passive.
** 'The user logged into their account,' not 'The account was logged into by the user.'
* Try to avoid unnecessary jargon
** If in doubt, explain it or link to a good explanation
* Try to be positive
** We want them to understand the facts but not scare them unnecessarily. We are part of the solution, not the problem.

26
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.eslintcache
incomplete-rules-repo/

50
frontend/README.adoc Normal file
View File

@ -0,0 +1,50 @@
= Search page for the rule repository
This is a single-page React application that indexes the rule repository and
allows you to run flexible search through all of the rules and rule drafts,
and render the rule specifications in HTML.
The render is not guaranteed to match the rule rendering in the products,
but it is a good proxy.
== Local deployment
Make sure you have NodeJs and `npm` available. Tested with NPM v10.2.3 and NodeJS v18.19.0.
Install dependencies
[source,shell]
----
npm install
----
If you have some non-js dependencies missing, this might fail.
Possibly missing non-js dependencies include OpenSSL, libuv, libssh2, KRB5.
Once you succeed in installing the dependencies you are ready to predeploy.
[source,shell]
----
npm run predeploy
----
This command builds the database of the rule specifications.
NOTE: If the script fails to clone or fetch due to an SSL certificate failure
and your network uses a custom CA certificate you might need to make sure it is installed
in the accessible place.
As a workaround you can https://github.com/nodegit/nodegit/issues/1742[disable the certificate check].
NOTE: In the predeploy step (specifically the `prepare-rules` part of it) the script fetches all the open PRs locally.
You might want to set `GITHUB_TOKEN` to your personal GitHub token
to avoid GitHub throttling your requests during the predeploy stage.
Now you can run it locally:
[source,shell]
----
npm start
----
This should open https://localhost:3000/rspec in your default browser with the rule search page.

20336
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

72
frontend/package.json Normal file
View File

@ -0,0 +1,72 @@
{
"name": "rspec",
"version": "0.1.0",
"private": true,
"homepage": "http://sonarsource.github.io/rspec",
"dependencies": {
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "^4.0.0-alpha.61",
"@octokit/rest": "^18.12.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@types/jest": "^26.0.24",
"@types/node": "^12.20.55",
"@types/react": "^16.14.49",
"@types/react-dom": "^16.9.20",
"asciidoctor": "^2.2.6",
"html-react-parser": "^1.4.14",
"lunr": "^2.3.9",
"node-html-parser": "^5.4.2",
"nodegit": "^0.28.0-alpha.24",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-highlight": "^0.14.0",
"react-router-dom": "^5.3.4",
"react-scripts": "^5.0.1",
"setimmediate": "^1.0.5",
"string-strip-html": "^8.5.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"verror": "^1.10.1",
"web-vitals": "^0.2.4",
"winston": "^3.11.0",
"yargs": "^16.2.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"prepare-rules": "ts-node -P tsconfig-rules.json ./src/deployment/index.ts",
"predeploy": "npm run prepare-rules && npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@testing-library/react-hooks": "^7.0.2",
"@types/lunr": "^2.3.5",
"@types/nodegit": "^0.27.10",
"@types/react-highlight": "^0.12.6",
"@types/react-router-dom": "^5.3.3",
"tmp-promise": "^3.0.3"
}
}

File diff suppressed because it is too large Load Diff

BIN
frontend/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html prefix="og: https://ogp.me/ns#" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Rules Repository Search Page"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo128.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.png" or "favicon.png", "%PUBLIC_URL%/favicon.png" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>RSPEC</title>
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="%PUBLIC_URL%/favicon.png" />
<meta name="twitter:title" content="RSPEC" />
<meta name="twitter:description" content="Rules Repository Search Page" />
<!-- Open Graph -->
<meta property="og:title" content="RSPEC" />
<meta property="og:description" content="Rules Repository Search Page" />
<meta property="og:type" content="website" />
<meta property="og:url" content="%PUBLIC_URL%" />
<meta property="og:image" content="%PUBLIC_URL%/favicon.png" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
frontend/public/logo128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "RSPEC",
"name": "Rules Repository Search Page",
"icons": [
{
"src": "favicon.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo128.png",
"type": "image/png",
"sizes": "128x128"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,10 @@
sonar.projectKey=rspec-frontend
sonar.projectName=rspec-frontend
sonar.sources=src
sonar.exclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
sonar.tests=src
sonar.test.inclusions=**/*.test.js,**/*.test.jsx,**/*.test.ts,**/*.test.tsx
sonar.javascript.coveragePlugin=lcov
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.cpd.exclusions=src/deployment/__tests__/resources/**

View File

@ -0,0 +1,9 @@
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles((theme) => ({
root: {
textAlign: 'left',
backgroundColor: 'white',
},
appBarSpacer: theme.mixins.toolbar,
}));

36
frontend/src/App.tsx Normal file
View File

@ -0,0 +1,36 @@
import React from 'react';
import CssBaseline from '@material-ui/core/CssBaseline';
import { Box } from '@material-ui/core';
import useStyles from './App.style';
import {
HashRouter as Router,
Switch,
Route
} from "react-router-dom";
import {RulePage} from "./RulePage";
import {SearchPage} from "./SearchPage";
import TopBar from "./TopBar";
function App() {
const classes = useStyles();
return (
<CssBaseline>
<div className={classes.root}>
<TopBar/>
<Box>
<Router basename="/rspec">
<Switch>
<Route path="/:ruleid/:language?" component={RulePage} />
<Route>
<SearchPage/>
</Route>
</Switch>
</Router>
</Box>
</div>
</CssBaseline>
);
}
export default App;

470
frontend/src/RulePage.tsx Normal file
View File

@ -0,0 +1,470 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
import Typography from '@material-ui/core/Typography';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Box from '@material-ui/core/Box';
import Tooltip from '@material-ui/core/Tooltip';
import { createTheme, Link, ThemeProvider } from '@material-ui/core';
import Highlight from 'react-highlight';
import { Link as RouterLink, useHistory } from 'react-router-dom';
import { RULE_STATE, useRuleCoverage } from './utils/useRuleCoverage';
import { useFetch } from './utils/useFetch';
import RuleMetadata, { Version, Coverage } from './types/RuleMetadata';
import parse, { attributesToProps, domToReact, DOMNode, Element } from 'html-react-parser';
import VisibilityOffOutlinedIcon from '@material-ui/icons/VisibilityOffOutlined';
import './hljs-humanoid-light.css';
const PARAMETER_INTERNAL_MARGIN = 0.5;
const useStyles = makeStyles((theme) => ({
'@global': {
h1: {
fontSize: '1.6rem',
fontWeight: 500,
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3)
},
h2: {
color: '#0B3C62',
fontSize: '1.2rem'
},
h3: {
fontSize: '1rem',
color: '#25699D'
},
hr: {
color: '#F9F9FB'
},
'.sidebarblock': {
'& .title': {
marginTop: theme.spacing(2),
color: '#25699D'
},
'& pre': {
marginLeft: '1rem',
marginTop: theme.spacing(PARAMETER_INTERNAL_MARGIN),
marginBottom: theme.spacing(PARAMETER_INTERNAL_MARGIN)
},
'& p': {
marginLeft: '1rem',
marginTop: theme.spacing(PARAMETER_INTERNAL_MARGIN),
marginBottom: theme.spacing(PARAMETER_INTERNAL_MARGIN)
}
}
},
ruleBar: {
borderBottom: '1px solid lightgrey',
},
ruleid: {
textAlign: 'center',
marginTop: theme.spacing(3),
marginBottom: theme.spacing(3),
color: 'black'
},
ruleidLink: {
color: 'inherit',
},
title: {
textAlign: 'justify',
marginTop: theme.spacing(4),
marginBottom: theme.spacing(4),
},
avoid: {
textDecoration: 'line-through'
},
coverage: {
marginBottom: theme.spacing(3),
},
description: {
textAlign: 'justify',
// marginBottom: theme.spacing(3),
},
// style used to center the tabs when there too few of them to fill the container
tabRoot: {
justifyContent: 'center'
},
tabScroller: {
flexGrow: 0
},
unimplemented: {
color: 'red'
},
tab: {
display: 'flex',
'&::before': {
content: '""',
display: 'block',
width: theme.spacing(1),
height: theme.spacing(1),
marginRight: theme.spacing(1),
borderRadius: theme.spacing(1),
},
'& > .MuiTab-wrapper': {
width: 'auto',
}
},
coveredTab: {
'&::before': {
backgroundColor: RULE_STATE['covered'].color,
}
},
targetedTab: {
'&::before': {
borderColor: RULE_STATE['targeted'].color,
border: '1px solid',
backgroundColor: 'transparent'
}
},
removedTab: {
'&::before': {
backgroundColor: RULE_STATE['removed'].color,
}
},
closedTab: {
'&::before': {
backgroundColor: RULE_STATE['closed'].color,
}
},
deprecatedTab: {
'&::before': {
backgroundColor: RULE_STATE['deprecated'].color,
}
},
}));
const theme = createTheme({});
type UsedStyles = ReturnType<typeof useStyles>;
const languageToJiraProject = new Map(Object.entries({
'PYTHON': 'SONARPY',
'ABAP': 'SONARABAP',
'AZURE_RESOURCE_MANAGER': 'SONARIAC',
'CFAMILY': 'CPP',
'DART': 'DART',
'DOCKER': 'SONARIAC',
'JAVA': 'SONARJAVA',
'JCL': 'SONARJCL',
'COBOL': 'SONARCOBOL',
'FLEX': 'SONARFLEX',
'HTML': 'SONARHTML',
'PHP': 'SONARPHP',
'PLI': 'SONARPLI',
'PLSQL': 'PLSQL',
'RPG': 'SONARRPG',
'APEX': 'SONARAPEX',
'RUBY': 'SONARRUBY',
'RUST': 'SKUNK',
'KOTLIN': 'SONARKT',
'SCALA': 'SONARSCALA',
'GO': 'SONARGO',
'SECRETS': 'SONARTEXT',
'SWIFT': 'SONARSWIFT',
'TSQL': 'TSQL',
'VB6': 'VB6',
'XML': 'SONARXML',
'CLOUDFORMATION': 'SONARIAC',
'TERRAFORM': 'SONARIAC',
'KUBERNETES': 'SONARIAC',
'TEXT': 'SONARTEXT',
'ANSIBLE': 'SONARIAC',
}));
const languageToGithubProject = new Map(Object.entries({
'ABAP': 'sonar-abap',
'AZURE_RESOURCE_MANAGER': 'sonar-iac',
'CSHARP': 'sonar-dotnet',
'DART': 'sonar-dart',
'DOCKER': 'sonar-iac',
'VBNET': 'sonar-dotnet',
'JAVASCRIPT': 'SonarJS',
'TYPESCRIPT': 'SonarJS',
'SWIFT': 'sonar-swift',
'KOTLIN': 'sonar-kotlin',
'GO': 'sonar-go',
'SCALA': 'sonar-scala',
'RUBY': 'sonar-ruby',
'RUST': 'sonar-rust',
'APEX': 'sonar-apex',
'HTML': 'sonar-html',
'COBOL': 'sonar-cobol',
'VB6': 'sonar-vb',
'JAVA': 'sonar-java',
'JCL': 'sonar-jcl',
'PLI': 'sonar-pli',
'CFAMILY': 'sonar-cpp',
'CSS': 'sonar-css',
'FLEX': 'sonar-flex',
'PHP': 'sonar-php',
'PLSQL': 'sonar-plsql',
'PYTHON': 'sonar-python',
'RPG': 'sonar-rpg',
'TSQL': 'sonar-tsql',
'XML': 'sonar-xml',
'CLOUDFORMATION': 'sonar-iac',
'TERRAFORM': 'sonar-iac',
'KUBERNETES': 'sonar-iac',
'SECRETS': 'sonar-text',
'TEXT': 'sonar-text',
'ANSIBLE': 'sonar-iac-enterprise',
}));
function ticketsAndImplementationPRsLinks(ruleNumber: string, title: string, language?: string) {
if (language) {
const upperCaseLanguage = language.toUpperCase();
const jiraProject = languageToJiraProject.get(upperCaseLanguage);
const githubProject = languageToGithubProject.get(upperCaseLanguage);
const titleWihoutQuotes = title.replace(/"/g, "'");
const implementationPRsLink = (
<Link href={`https://github.com/SonarSource/${githubProject}/pulls?q=is%3Apr+"S${ruleNumber}"+OR+"RSPEC-${ruleNumber}"`}>
Implementation Pull Requests
</Link>
);
if (jiraProject !== undefined) {
const ticketsLink = (
<Link href={`https://jira.sonarsource.com/issues/?jql=project%20%3D%20${jiraProject}%20AND%20(text%20~%20%22S${ruleNumber}%22%20OR%20text%20~%20%22RSPEC-${ruleNumber}%22%20OR%20text%20~%20"${titleWihoutQuotes}")`}>
Implementation tickets on Jira
</Link>
);
return {ticketsLink, implementationPRsLink};
} else {
const ticketsLink = (
<Link href={`https://github.com/SonarSource/${githubProject}/issues?q=is%3Aissue+"S${ruleNumber}"+OR+"RSPEC-${ruleNumber}"`}>
Implementation issues on GitHub
</Link>
);
return {ticketsLink, implementationPRsLink};
}
} else {
const ticketsLink = (<div>Select a language to see the implementation tickets</div>);
const implementationPRsLink = (<div>Select a language to see the implementation pull requests</div>);
return {ticketsLink, implementationPRsLink};
}
}
const RuleThemeProvider: React.FC = ({ children }) => {
useStyles();
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
interface PageMetadata {
title: string;
languagesTabs: JSX.Element[] | null;
avoid: boolean;
prUrl: string | undefined;
branch: string;
coverage: Coverage;
isInQualityProfile: boolean;
jsonString: string | undefined;
}
function usePageMetadata(ruleid: string, language: string, classes: UsedStyles): PageMetadata {
const metadataUrl = `${process.env.PUBLIC_URL}/rules/${ruleid}/${language ?? 'default'}-metadata.json`;
let [metadataJSON, metadataError, metadataIsLoading] = useFetch<RuleMetadata>(metadataUrl);
let coverage: Coverage = 'Loading...';
let title = 'Loading...';
let avoid = false;
let isInQualityProfile = false;
let metadataJSONString;
let languagesTabs = null;
let prUrl: string | undefined = undefined;
let branch = 'master';
const { ruleCoverage, allLangsRuleCoverage, ruleStateInAnalyzer } = useRuleCoverage();
if (metadataJSON && !metadataIsLoading && !metadataError) {
title = metadataJSON.title;
if ('prUrl' in metadataJSON) {
prUrl = metadataJSON.prUrl;
}
branch = metadataJSON.branch;
metadataJSON.languagesSupport.sort((a, b) => a.name.localeCompare(b.name));
const ruleStates = metadataJSON.languagesSupport.map(({ name, status }) => ({
name,
ruleState: ruleStateInAnalyzer(name, metadataJSON!.allKeys, status)
}));
languagesTabs = ruleStates.map(({ name, ruleState }) => {
const classNames = classes.tab + ' ' + (classes as any)[ruleState + 'Tab'];
return <Tab key={name} label={name} value={name} className={classNames} />;
});
avoid = !ruleStates.some(({ ruleState }) => ruleState === 'covered' || ruleState === 'targeted');
metadataJSONString = JSON.stringify(metadataJSON, null, 2);
const coverageMapper = (key: string, range: Version ): JSX.Element => {
if (typeof range === 'string') {
return (
<li key={key} >{key}: {range}</li>
);
} else {
return (
<li key={key} >Not covered for {key} anymore. Was covered from {range.since} to {range.until}.</li>
);
}
};
if (language) {
coverage = ruleCoverage(language, metadataJSON.allKeys, coverageMapper);
} else {
coverage = allLangsRuleCoverage(metadataJSON.allKeys, coverageMapper);
}
isInQualityProfile = metadataJSON.defaultQualityProfiles.length > 0;
}
if (coverage !== 'Not Covered') {
prUrl = undefined;
branch = 'master';
}
return {
title,
languagesTabs,
avoid,
prUrl,
branch,
coverage,
isInQualityProfile,
jsonString: metadataJSONString
};
}
function getRspecPath(rspecId: string, language?: string) {
return '/rspec#/rspec/' + rspecId;
}
function useDescription(metadata: PageMetadata, ruleid: string, language?: string) {
const editOnGithubUrl =
`https://github.com/SonarSource/rspec/blob/${metadata.branch}/rules/${ruleid}${language ? '/' + language : ''}`;
function htmlReplacement(domNode: Element) {
if (domNode.name === 'a' && domNode.attribs?.['data-rspec-id']) {
const props = attributesToProps(domNode.attribs);
return <a href={getRspecPath(domNode.attribs['data-rspec-id'], language)} {...props}>
{domToReact(domNode.children)}
</a>;
}
if (domNode.name === 'code' && domNode.attribs?.['data-lang']) {
return <Highlight className={domNode.attribs['data-lang']}>
{domToReact(domNode.children)}
</Highlight>;
}
return undefined; // No modification.
}
const descUrl = `${process.env.PUBLIC_URL}/rules/${ruleid}/${language ?? 'default'}-description.html`;
const [descHTML, descError, descIsLoading] = useFetch<string>(descUrl, false);
if (descHTML !== null && !descIsLoading && !descError) {
return <div>
{parse(descHTML, { replace: (d: DOMNode) => htmlReplacement(d as Element) })}
<hr />
<a href={editOnGithubUrl}>Edit on Github</a><br />
<hr />
<Highlight className='json'>{metadata.jsonString}</Highlight>
</div>;
}
return <div>Loading...</div>;
}
export function RulePage(props: any) {
// language can be absent
const {ruleid, language} = props.match.params;
document.title = ruleid;
const history = useHistory();
function handleLanguageChange(event: any, lang: string) {
history.push(`/${ruleid}/${lang}`);
}
const classes = useStyles();
const metadata = usePageMetadata(ruleid, language, classes);
const description = useDescription(metadata, ruleid, language);
let prLink = <></>;
if (metadata.prUrl) {
prLink = <div>
<span className={classes.unimplemented}>Not implemented (see <a href={metadata.prUrl}>PR</a>)</span>
</div>;
}
const ruleNumber = ruleid.substring(1);
const specificationPRsLink = (
<Link href={`https://github.com/SonarSource/rspec/pulls?q=is%3Apr+"S${ruleNumber}"+OR+"RSPEC-${ruleNumber}"`}>
Specification Pull Requests
</Link>
);
const {ticketsLink, implementationPRsLink} = ticketsAndImplementationPRsLinks(ruleNumber, metadata.title, language);
const tabsValue = language ? {'value' : language} : {'value': false};
return (
<div>
<div className={classes.ruleBar}>
<Container>
<Typography variant="h2" classes={{ root: classes.ruleid }}>
<Link className={`${classes.ruleidLink} ${metadata.avoid ? classes.avoid : ''}`}
component={RouterLink} to={`/${ruleid}`} underline="none">{ruleid}</Link>
</Typography>
<Typography variant="h4" classes={{ root: classes.ruleid }}>{prLink}</Typography>
<Tabs
{...tabsValue}
onChange={handleLanguageChange}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
scrollButtons="auto"
classes={{ root: classes.tabRoot, scroller: classes.tabScroller }}
>
{metadata.languagesTabs}
</Tabs>
</Container>
</div>
<RuleThemeProvider>
<Container maxWidth="md">
<h1>
{metadata.isInQualityProfile ? <></> : <><Tooltip title="Not in any Quality Profile"><VisibilityOffOutlinedIcon /></Tooltip> </>}
{metadata.title}
</h1>
<hr />
<Box className={classes.coverage}>
<h2>Covered Since</h2>
<ul>
{metadata.coverage}
</ul>
</Box>
<Box className={classes.coverage}>
<h2>Related Tickets and Pull Requests</h2>
<ul>
{specificationPRsLink}
</ul>
<ul>
{implementationPRsLink}
</ul>
<ul>
{ticketsLink}
</ul>
</Box>
<Box>
<Typography component={'span'} className={classes.description}>
{description}
</Typography>
</Box>
</Container>
</RuleThemeProvider>
</div>
);
}

240
frontend/src/SearchHit.tsx Normal file
View File

@ -0,0 +1,240 @@
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Chip from '@material-ui/core/Chip';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableContainer from '@material-ui/core/TableContainer';
import TableRow from '@material-ui/core/TableRow';
import { Link as RouterLink } from 'react-router-dom';
import { Link } from '@material-ui/core';
import { IndexedRule } from './types/IndexStore';
import { RULE_STATE, useRuleCoverage } from './utils/useRuleCoverage';
const CHIP_V_MARGIN = 0.5;
const TABLE_PADDING = 1;
const useStyles = makeStyles((theme) => ({
searchHit: {
},
ruleid: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
},
avoid: {
textDecoration: 'line-through'
},
language: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(2),
},
coveredLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(CHIP_V_MARGIN),
marginBottom: theme.spacing(CHIP_V_MARGIN),
backgroundColor: RULE_STATE['covered'].color,
'&:hover, &:focus': {
backgroundColor: RULE_STATE['covered'].darker
},
},
targetedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(CHIP_V_MARGIN),
marginBottom: theme.spacing(CHIP_V_MARGIN),
color: RULE_STATE['targeted'].color,
borderColor: RULE_STATE['targeted'].color,
'&:hover, &:focus': {
color: RULE_STATE['targeted'].darker,
borderColor: RULE_STATE['covered'].darker,
},
},
removedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(CHIP_V_MARGIN),
marginBottom: theme.spacing(CHIP_V_MARGIN),
backgroundColor: RULE_STATE['removed'].color,
'&:hover, &:focus': {
backgroundColor: RULE_STATE['removed'].darker
},
},
deprecatedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(CHIP_V_MARGIN),
marginBottom: theme.spacing(CHIP_V_MARGIN),
backgroundColor: RULE_STATE['deprecated'].color,
'&:hover, &:focus': {
backgroundColor: RULE_STATE['deprecated'].darker
},
},
closedLanguageChip: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(CHIP_V_MARGIN),
marginBottom: theme.spacing(CHIP_V_MARGIN),
backgroundColor: RULE_STATE['closed'].color,
'&:hover, &:focus': {
backgroundColor: RULE_STATE['closed'].darker
},
},
coveredTitle: {
borderColor: RULE_STATE['covered'].color,
padding: theme.spacing(TABLE_PADDING)
},
coveredMarker: {
padding: theme.spacing(TABLE_PADDING),
width: '100%'
},
targetedTitle: {
borderColor: RULE_STATE['covered'].color,
padding: theme.spacing(TABLE_PADDING)
},
targetedMarker: {
padding: theme.spacing(TABLE_PADDING),
width: '100%'
},
removedTitle: {
borderColor: RULE_STATE['removed'].color,
padding: theme.spacing(TABLE_PADDING)
},
removedMarker: {
padding: theme.spacing(TABLE_PADDING),
width: '100%'
},
deprecatedTitle: {
borderColor: RULE_STATE['deprecated'].color,
padding: theme.spacing(TABLE_PADDING)
},
deprecatedMarker: {
padding: theme.spacing(TABLE_PADDING),
width: '100%'
},
closedTitle: {
borderColor: RULE_STATE['closed'].color,
padding: theme.spacing(TABLE_PADDING)
},
closedMarker: {
padding: theme.spacing(TABLE_PADDING),
width: '100%'
}
}));
type SearchHitProps = {
data: IndexedRule
}
export function SearchHit(props: SearchHitProps) {
const { ruleStateInAnalyzer } = useRuleCoverage();
const classes = useStyles();
const coveredLanguages: JSX.Element[] = [];
const targetedLanguages: JSX.Element[] = [];
const removedLanguages: JSX.Element[] = [];
const deprecatedLanguages: JSX.Element[] = [];
const closedLanguages: JSX.Element[] = [];
const actualLanguages = props.data.supportedLanguages.filter(l => l.name !== 'default');
const ruleStates = actualLanguages.map(l => ({
lang: l.name,
ruleState: ruleStateInAnalyzer(l.name, props.data.all_keys, l.status)
}));
ruleStates.forEach(({lang, ruleState}) => {
const chip = <Link key={lang} component={RouterLink} to={`/${props.data.id}/${lang}`}
style={{ textDecoration: 'none' }}>
<Chip
classes={{root: (classes as any)[ruleState + 'LanguageChip']}}
label={lang}
color="primary"
variant={ruleState === 'targeted' ? 'outlined' : 'default'}
clickable
key="{lang}"
/>
</Link>;
switch(ruleState) {
case 'targeted':
targetedLanguages.push(chip);
break;
case 'removed':
removedLanguages.push(chip);
break;
case 'deprecated':
deprecatedLanguages.push(chip);
break;
case 'closed':
closedLanguages.push(chip);
break;
case 'covered':
default:
coveredLanguages.push(chip);
break;
}
});
const avoid = !ruleStates.some(({ ruleState }) => ruleState === 'targeted' || ruleState === 'covered');
const titles = props.data.titles.map(title => (
<Typography key={title} variant="body1" component="p" gutterBottom>
{title}
</Typography>
));
const coveredRow = coveredLanguages.length === 0 ? <></>
: <TableRow>
<TableCell classes={{ root: classes.coveredTitle }}>Covered</TableCell>
<TableCell classes={{ root: classes.coveredMarker }}>{coveredLanguages}</TableCell>
</TableRow>;
const targetedRow = targetedLanguages.length === 0 ? <></>
: <TableRow>
<TableCell classes={{ root: classes.targetedTitle }}>Targeted</TableCell>
<TableCell classes={{ root: classes.targetedMarker }}>{targetedLanguages}</TableCell>
</TableRow>;
const deprecatedRow = deprecatedLanguages.length === 0 ? <></>
: <TableRow>
<TableCell classes={{ root: classes.deprecatedTitle }}>Deprecated</TableCell>
<TableCell classes={{ root: classes.deprecatedMarker }}>{deprecatedLanguages}</TableCell>
</TableRow>;
const removedRow = removedLanguages.length === 0 ? <></>
: <TableRow>
<TableCell classes={{ root: classes.removedTitle }}>Removed</TableCell>
<TableCell classes={{ root: classes.removedMarker }}>{removedLanguages}</TableCell>
</TableRow>;
const closedRow = closedLanguages.length === 0 ? <></>
: <TableRow>
<TableCell classes={{ root: classes.closedTitle }}>Closed</TableCell>
<TableCell classes={{ root: classes.closedMarker }}>{closedLanguages}</TableCell>
</TableRow>;
return (
<Card variant="outlined" classes={{ root: classes.searchHit }}>
<CardContent>
<Typography key="rule-id" classes={{ root: `${classes.ruleid} ${avoid ? classes.avoid : ''}` }}
variant="h5" component="h5" gutterBottom>
<Link component={RouterLink} to={`/${props.data.id}`} data-testid={`search-hit-${props.data.id}`}>
<div> Rule {props.data.id} </div>
</Link>
</Typography>
{titles}
<TableContainer>
<Table >
<TableBody>
{coveredRow}
{targetedRow}
{removedRow}
{deprecatedRow}
{closedRow}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,31 @@
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles((theme) => ({
root: {
// marginTop: theme.spacing(1),
},
searchHitBox: {
marginBottom: theme.spacing(1),
},
searchBar: {
// backgroundColor: 'white'
borderBottom: '1px solid lightgrey',
paddingBottom: theme.spacing(1),
},
searchResults: {
marginTop: theme.spacing(3),
},
topRow: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between'
},
resultsCount: {
marginBottom: theme.spacing(2),
},
fullWidth: {
maxWidth: '120%',
flexBasis: '120%'
}
}));

267
frontend/src/SearchPage.tsx Normal file
View File

@ -0,0 +1,267 @@
import React, { KeyboardEvent } from 'react';
import Typography from '@material-ui/core/Typography';
import TextField from '@material-ui/core/TextField';
import MenuItem from '@material-ui/core/MenuItem';
import Container from '@material-ui/core/Container';
import Grid from '@material-ui/core/Grid';
import Pagination from '@material-ui/lab/Pagination';
import Box from '@material-ui/core/Box';
import useStyles from './SearchPage.style';
import { useSearch } from './utils/useSearch';
import { useFetch } from './utils/useFetch';
import {
SearchParamSetter,
useLocationSearch,
useLocationSearchState
} from './utils/routing';
import { SearchHit } from './SearchHit';
import { IndexedRule, IndexAggregates } from './types/IndexStore';
import { useHistory } from 'react-router-dom';
function correctResultsOrder(results: IndexedRule[], query: string): IndexedRule[] {
const upperCaseQuery = query.toLocaleUpperCase();
const reorderedResults: IndexedRule[] = [];
results.forEach(indexedRule => {
if (indexedRule.all_keys.includes(upperCaseQuery)) {
reorderedResults.unshift(indexedRule);
} else {
reorderedResults.push(indexedRule);
}
});
return reorderedResults;
}
export const SearchPage = () => {
document.title = 'Search';
const classes = useStyles();
const pageSize = 20;
const [query, setQuery] = useLocationSearchState('query', '');
const history = useHistory();
const [ruleType, setRuleType] = useLocationSearchState('types', 'ANY');
const allRuleTypes: Record<string,string> = {
'BUG': 'Bug',
'CODE_SMELL': 'Code Smell',
'SECURITY_HOTSPOT': 'Security Hotspot',
'VULNERABILITY': 'Vulnerability'
};
const [ruleTags, setRuleTags] = useLocationSearchState<string[]>('tags', [], value => value ? value.split(',') : []);
const [qualityProfiles, setQualityProfiles] = useLocationSearchState<string[]>('qualityProfiles', [], value => value ? value.split(',') : []);
const [ruleLang, setLanguage] = useLocationSearchState('lang', 'ANY');
const [pageNumber, setPageNumber] = useLocationSearchState('page', 1, parseInt);
const {setLocationSearch} = useLocationSearch();
const {results, numberOfHits, error, loading} = useSearch(query,
ruleType === 'ANY' ? null : ruleType,
ruleLang === 'ANY' ? null : ruleLang,
ruleTags,
qualityProfiles,
pageSize, pageNumber);
const totalPages = numberOfHits ? Math.ceil(numberOfHits/pageSize) : 0;
let allRuleTags:string[] = [];
let allLangs:string[] = [];
let allQualityProfiles = ['Sonar way', 'Sonar way recommended'];
const aggregatesDataUrl = `${process.env.PUBLIC_URL}/rules/rule-index-aggregates.json`;
const [aggregatesData, aggregatesDataError, aggregatesDataIsLoading] = useFetch<IndexAggregates>(aggregatesDataUrl);
if (aggregatesData && !aggregatesDataIsLoading && !aggregatesDataError) {
allRuleTags = Object.keys(aggregatesData.tags).sort();
allLangs = Object.keys(aggregatesData.langs).sort();
allQualityProfiles = Object.keys(aggregatesData.qualityProfiles).sort();
}
let resultsDisplay: string|JSX.Element[] = 'No rule found...';
if (loading) {
resultsDisplay = 'Searching';
} else if (results.length > 0) {
resultsDisplay = correctResultsOrder(results, query).map(indexedRule => <Box key={indexedRule.id} className={classes.searchHitBox}>
<SearchHit key={indexedRule.id} data={indexedRule}/>
</Box>);
}
const paramSetters: Record<string, SearchParamSetter<any>> = {
types: setRuleType,
tags: setRuleTags,
qualityProfiles: setQualityProfiles,
lang:setLanguage,
query: setQuery
};
function handleUpdate(field: string) {
return function(event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) {
if (pageNumber > 1) {
const uriSearch: Record<string, any> = {
query: query, types: ruleType, tags: ruleTags, qualityProfiles: qualityProfiles, lang: ruleLang, page: 1
};
uriSearch[field] = event.target.value;
setLocationSearch(uriSearch);
} else {
paramSetters[field](event.target.value, {push: false});
}
}
}
function handleKeyup(event: KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
const query = (event.target as HTMLTextAreaElement).value;
if (/^(S|RSPEC-?)?\d{3,}$/i.exec(query)) {
if (0 < results.length) {
history.push(correctResultsOrder(results, query)[0].id);
}
} else if (1 === results.length) {
history.push(results[0].id);
}
}
}
return (
<div className={classes.root}>
<div className={classes.searchBar}>
<Container maxWidth="md">
<Grid container>
<Grid item xs={12}>
<TextField
id="title-query"
label="Rule Title and Description"
placeholder="Search in rule titles and descriptions"
fullWidth
margin="normal"
InputLabelProps={{
shrink: true,
}}
variant="outlined"
value={query}
onChange={handleUpdate('query')}
onKeyUp={handleKeyup}
error={!!error}
helperText={error}
autoFocus
/>
</Grid>
<Grid item xs={12} container spacing={3} className={classes.fullWidth}>
<Grid item xs={3}>
<TextField
select
fullWidth
margin="normal"
variant="outlined"
size="small"
label="Rule type"
value={ruleType}
onChange={handleUpdate('types')}
data-testid="rule-type"
>
<MenuItem key="Any" value="ANY">
Any
</MenuItem>
{Object.keys(allRuleTypes).map((ruleType) => (
<MenuItem key={ruleType} value={ruleType} data-testid={`rule-type-${ruleType}`}>
{allRuleTypes[ruleType]}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={5}>
<TextField
select
fullWidth
size="small"
SelectProps={{
multiple: true,
renderValue: (selected: any) => {
return selected.join(', ');
}
}}
margin="normal"
variant="outlined"
label="Rule Tags"
value={ruleTags}
onChange={handleUpdate('tags')}
data-testid="rule-tags"
>
{allRuleTags.map((ruleTag) => (
<MenuItem key={ruleTag} value={ruleTag} data-testid={`rule-tag-${ruleTag}`}>
{ruleTag}
</MenuItem>
))}
</TextField>
</Grid>
<Grid item xs={4}>
<TextField
select
fullWidth
margin="normal"
size="small"
variant="outlined"
label="Language"
value={allLangs.length === 0 ? '' : ruleLang}
onChange={handleUpdate('lang')}
data-testid="rule-language"
>
<MenuItem key="Any" value="ANY">
Any
</MenuItem>
{allLangs.map((lang) => (
<MenuItem key={lang} value={lang} data-testid={`rule-language-${lang}`}>
{lang}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
<Grid item xs={12}>
<TextField
select
fullWidth
size="small"
SelectProps={{
multiple: true,
renderValue: (selected: any) => {
return selected.join(', ');
}
}}
margin="normal"
variant="outlined"
label="Default Quality Profiles"
value={qualityProfiles}
onChange={handleUpdate('qualityProfiles')}
data-testid="rule-default-quality-profile"
>
{allQualityProfiles.map((qualityProfile) => (
<MenuItem key={qualityProfile} value={qualityProfile}
data-testid={`rule-qual-profile-${qualityProfile}`}>
{qualityProfile}
</MenuItem>
))}
</TextField>
</Grid>
</Grid>
</Container>
</div>
<div className={classes.searchResults}>
<Container maxWidth="md">
<Grid container spacing={3}>
<Grid item xs={12}>
<Box key="total-num" className={classes.topRow}>
<Box className={classes.resultsCount}>
<Typography variant="subtitle1">Number of rules found: {numberOfHits}</Typography>
</Box>
</Box>
{resultsDisplay}
<Pagination count={totalPages} page={pageNumber} siblingCount={2}
onChange={(event, value) => setPageNumber(value)}
/>
</Grid>
</Grid>
</Container>
</div>
</div>
)
}

View File

@ -0,0 +1,18 @@
import { makeStyles } from '@material-ui/core/styles';
export default makeStyles((theme) => ({
root: {
flexGrow: 1,
marginBottom: '13px', // Quickfix. For some reason the topbar overlaps on the content.
backgroundColor: '#c72b28',
},
homeButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
unimplemented: {
color: '#FFFFFF',
},
}));

29
frontend/src/TopBar.tsx Normal file
View File

@ -0,0 +1,29 @@
import React from 'react';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import Button from '@material-ui/core/Button';
import HomeIcon from '@material-ui/icons/Home';
import useStyles from './TopBar.style';
export default function TopBar() {
const classes = useStyles();
return (
<AppBar position="static" className={classes.root}>
<Toolbar>
<IconButton edge="start" className={classes.homeButton} color="inherit" aria-label="menu" href="/rspec">
<HomeIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
SonarSource Rule Specifications
</Typography>
<Button href="https://github.com/SonarSource/rspec/pulls?q=is%3Aopen+is%3Apr+%22Create+rule%22">
<span className={classes.unimplemented} > Rules under specification </span>
</Button>
</Toolbar>
</AppBar>
);
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from '../App';
beforeEach(() => {
window.history.pushState({}, 'RSPEC Search Page', '/rspec/#/rspec/');
});
test('renders see the GH PR link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/Rules under specification/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,230 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { Location, createMemoryHistory } from 'history';
import { render } from '@testing-library/react';
import { Router } from 'react-router-dom';
import { generateRulesDescription } from '../deployment/description';
import { generateRulesMetadata } from '../deployment/metadata';
import { createIndexFiles } from '../deployment/searchIndex';
import { createFiles } from '../deployment/testutils';
import { fetchMock, normalize, FetchResult } from '../testutils';
import { SearchPage } from '../SearchPage';
// The CI system is a bit slow. Increase timeout to avoid random failures.
jest.setTimeout(20000);
function readJson(filepath: string) {
const content = fs.readFileSync(filepath);
return JSON.parse(content.toString());
}
const Paths = {
src: '',
dst: '',
index: '',
store: '',
aggregates: '',
};
beforeAll(() => {
Paths.src = fs.mkdtempSync(path.join(os.tmpdir(), 'end-to-end-testing-src-'));
Paths.dst = fs.mkdtempSync(path.join(os.tmpdir(), 'end-to-end-testing-dst-'));
createFiles(Paths.src, {
'S100/rule.adoc': 'Generic content',
'S100/metadata.json': JSON.stringify({
title: 'Generic Rule S100 Title',
tags: ['confusing'],
type: 'CODE_SMELL',
defaultQualityProfiles: [
'Sonar way'
],
}),
'S100/java/rule.adoc': 'Java Content',
'S100/java/metadata.json': JSON.stringify({
title: 'Java Rule S100 Title',
status: 'ready',
}),
'S200/rule.adoc': 'Generic content',
'S200/metadata.json': JSON.stringify({
title: 'Generic Rule S200 Title',
tags: ['confusing'],
type: 'CODE_SMELL',
}),
'S200/python/rule.adoc': 'Python content',
'S200/python/metadata.json': JSON.stringify({
title: 'Python Rule S200 Title',
tags: ['pep8'],
status: 'ready',
}),
'S200/java/rule.adoc': 'Java Content',
'S200/java/metadata.json': JSON.stringify({
title: 'Java Rule S200 Title',
type: 'BUG',
status: 'ready',
}),
'S501/rule.adoc': 'Generic content, no active language',
'S501/metadata.json': JSON.stringify({
title: 'Rule S501 Rule is closed with no language-specific specification',
type: 'CODE_SMELL',
status: 'closed',
}),
});
generateRulesMetadata(Paths.src, Paths.dst);
generateRulesDescription(Paths.src, Paths.dst);
createIndexFiles(Paths.dst);
Paths.index = path.join(Paths.dst, 'rule-index.json');
Paths.store = path.join(Paths.dst, 'rule-index-store.json');
Paths.aggregates = path.join(Paths.dst, 'rule-index-aggregates.json');
expect(fs.existsSync(Paths.index)).toBeTruthy();
expect(fs.existsSync(Paths.store)).toBeTruthy();
expect(fs.existsSync(Paths.aggregates)).toBeTruthy();
});
afterAll(() => {
// FIXME replace with fs.rm(path, { recursive: true, force: true }) when upgrading node to 14.14 or later.
fs.rmdirSync(Paths.dst, { recursive: true });
fs.rmdirSync(Paths.src, { recursive: true });
});
beforeEach(() => {
const index = readJson(Paths.index);
const indexStore = readJson(Paths.store);
const indexAggregates = readJson(Paths.aggregates);
const rootUrl = process.env.PUBLIC_URL;
const mockUrls = {} as Record<string, FetchResult>;
mockUrls[`${rootUrl}/rules/rule-index.json`] = { json: normalize(index) };
mockUrls[`${rootUrl}/rules/rule-index-store.json`] = { json: normalize(indexStore) };
mockUrls[`${rootUrl}/rules/rule-index-aggregates.json`] = { json: normalize(indexAggregates) };
mockUrls[`${rootUrl}/covered_rules.json`] = {
json: {
'JAVA': { 'S100': 'ver1', 'S200': 'ver2' },
'PY': { 'S200': 'ver1' },
}
};
jest.spyOn(global, 'fetch').mockImplementation(fetchMock(mockUrls));
});
afterEach(() => {
(global.fetch as any).mockClear();
});
const defaultUseLocation = jest.requireActual('react-router-dom').useLocation;
let fakeLocation: Location | undefined = undefined;
function mockUseLocation() {
return fakeLocation ?? defaultUseLocation();
}
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom') as {},
useLocation: mockUseLocation,
}));
beforeEach(() => {
fakeLocation = undefined;
});
function generateFakeLocation(search: string): Location {
return {
pathname: '/somepath',
search: search,
state: null,
hash: 'somehash',
key: undefined,
};
}
const history = createMemoryHistory();
describe('The main search page display correct results', () => {
test('Empty query', async () => {
expect(fakeLocation).toBeUndefined();
const { findByText, findAllByText } = render(<Router history={history} > <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 3\b/);
await findByText(/Java Rule S100 Title/);
await findByText(/Python Rule S200 Title/);
await findByText(/Java Rule S200 Title/);
await findByText(/Rule S501 Rule is closed with no language-specific specification/);
});
test('Query by title: Java.', async () => {
fakeLocation = generateFakeLocation('query=Java');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 2/);
await findByText(/Java Rule S100 Title/);
await findByText(/Python Rule S200 Title/);
await findByText(/Java Rule S200 Title/);
});
test('Query by title: S501.', async () => {
fakeLocation = generateFakeLocation('query=S501');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 1/);
await findByText(/Rule S501 Rule is closed with no language-specific specification/);
});
test('Query by type: BUG', async () => {
fakeLocation = generateFakeLocation('types=BUG');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 1/);
await findByText(/Python Rule S200 Title/);
await findByText(/Java Rule S200 Title/);
});
test('Query by type: VULNERABILITY', async () => {
fakeLocation = generateFakeLocation('types=VULNERABILITY');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 0/);
});
test('Query by tag: confusing', async () => {
fakeLocation = generateFakeLocation('tags=confusing');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 2/);
await findByText(/Java Rule S100 Title/);
await findByText(/Python Rule S200 Title/);
await findByText(/Java Rule S200 Title/);
});
test('Query by tag: confusing & pep8', async () => {
fakeLocation = generateFakeLocation('tags=confusing,pep8');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 1/);
await findByText(/Python Rule S200 Title/);
await findByText(/Java Rule S200 Title/);
});
test('Query by language: python', async () => {
fakeLocation = generateFakeLocation('lang=python');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 1/);
await findByText(/Python Rule S200 Title/);
await findByText(/Java Rule S200 Title/);
});
test('Advanced query', async () => {
fakeLocation = generateFakeLocation('qualityProfiles=Sonar way&query=Java&tags=confusing');
const { findByText, findAllByText } = render(<Router history={history}> <SearchPage /></Router >);
expect(await findAllByText(/Rule Title and Description/)).toHaveLength(1 + 1); // <label> + <span>
await findByText(/Number of rules found: 1/);
await findByText(/Java Rule S100 Title/);
});
});

View File

@ -0,0 +1,95 @@
import React from 'react';
import fs from 'fs';
import path from 'path';
import { render } from '@testing-library/react';
import { RulePage } from '../RulePage';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { fetchMock } from '../testutils'
const rulesPath = path.join(__dirname, '..', 'deployment', '__tests__', 'resources', 'metadata');
function readRuleFile(ruleId: string, filename: string) {
return fs.readFileSync(path.join(rulesPath, ruleId, filename)).toString();
}
beforeEach(() => {
const specS1000 = readRuleFile('S1000', 'cfamily-description.html');
const metadataS1000 = readRuleFile('S1000', 'cfamily-metadata.json');
const specS1007 = readRuleFile('S1007', 'default-description.html');
const metadataS1007 = readRuleFile('S1007', 'default-metadata.json');
const specS3457 = readRuleFile('S3457', 'csharp-description.html');
const metadataS3457 = readRuleFile('S3457', 'csharp-metadata.json');
const rootUrl = process.env.PUBLIC_URL;
let mockUrls: {[index: string]:any} = {};
mockUrls[`${rootUrl}/rules/S1000/cfamily-description.html`] = {text: specS1000};
mockUrls[`${rootUrl}/rules/S1000/cfamily-metadata.json`] = {json: JSON.parse(metadataS1000)};
mockUrls[`${rootUrl}/rules/S1007/default-description.html`] = {text: specS1007};
mockUrls[`${rootUrl}/rules/S1007/default-metadata.json`] = {json: JSON.parse(metadataS1007)};
mockUrls[`${rootUrl}/rules/S3457/csharp-description.html`] = {text: specS3457};
mockUrls[`${rootUrl}/rules/S3457/csharp-metadata.json`] = {json: JSON.parse(metadataS3457)};
mockUrls[`${rootUrl}/rules/S3457/default-description.html`] = {text: specS3457};
mockUrls[`${rootUrl}/rules/S3457/default-metadata.json`] = {json: JSON.parse(metadataS3457)};
mockUrls[`${rootUrl}/covered_rules.json`] = {json:
{'ABAP': {'S100': 'ver1', 'S200': 'ver2'},
'CSH' : {'S3457': 'c#1'},
'C': {'S100': 'c1', 'S234': {'since': 'c2', 'until': 'c3'}}}
};
jest.spyOn(global, 'fetch').mockImplementation(fetchMock(mockUrls) as jest.Mocked<typeof fetch>);
});
afterEach(() => {
global.fetch.mockClear();
});
test('renders cfamily version of S1000', async () => {
const history = createMemoryHistory();
history.push('/rspec/#/rspec/S1000/cfamily');
const match = {params: {ruleid: "S1000", language: "cfamily"}};
const { findByText, asFragment } = render(<Router history={history}>
<RulePage match={match} />
</Router>);
await findByText(/S1000/);
await findByText(/Implementation tickets on Jira/);
// some random phrase from the rule specification
await findByText(/7-3-3 - There shall be no unnamed namespaces in header files./);
expect(asFragment()).toMatchSnapshot();
});
test('renders C# version of S3457 (using GH for issues instead of Jira)', async () => {
const history = createMemoryHistory();
history.push('/rspec/#/rspec/S3457/csharp');
const match = {params: {ruleid: "S3457", language: "csharp"}};
const { findByText, asFragment } = render(<Router history={history}>
<RulePage match={match} />
</Router>);
await findByText(/S3457/);
await findByText(/Implementation issues on GitHub/);
expect(asFragment()).toMatchSnapshot();
});
test('renders generic version of S3457', async () => {
const history = createMemoryHistory();
history.push('/rspec/#/rspec/S3457');
const match = {params: {ruleid: "S3457"}};
const { findAllByText, asFragment } = render(<Router history={history}>
<RulePage match={match} />
</Router>);
await findAllByText(/S3457/);
await findAllByText(/cfamily/);
await findAllByText(/csharp/);
expect(asFragment()).toMatchSnapshot();
});
test('renders closed rule S1007', async () => {
const history = createMemoryHistory();
history.push('/rspec/#/rspec/S1007');
const match = {params: {ruleid: "S1007"}};
const { findAllByText, asFragment } = render(<Router history={history}>
<RulePage match={match} />
</Router>);
await findAllByText(/S1007/);
await findAllByText(/cfamily/);
await findAllByText(/closed/);
expect(asFragment()).toMatchSnapshot();
});

View File

@ -0,0 +1,220 @@
import path from 'path';
import { render, waitFor, fireEvent, within } from '@testing-library/react';
import { SearchPage } from '../SearchPage';
import { buildIndexStore, buildSearchIndex } from '../deployment/searchIndex';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { fetchMockObject, normalize } from '../testutils';
// The CI system is a bit slow. Increase timeout to avoid random failures.
jest.setTimeout(20000);
function genMockUrls() {
const rulePath = path.join(__dirname, '..', 'deployment', '__tests__', 'resources', 'metadata');
const [indexStore, indexAggregates] = buildIndexStore(rulePath);
const searchIndex: lunr.Index = buildSearchIndex(indexStore);
const rootUrl = process.env.PUBLIC_URL;
let mockUrls: {[index: string]: any} = {};
mockUrls[`${rootUrl}/rules/rule-index.json`] = {json: normalize(searchIndex)};
mockUrls[`${rootUrl}/rules/rule-index-store.json`] = {json: normalize(indexStore)};
mockUrls[`${rootUrl}/rules/rule-index-aggregates.json`] = {json: normalize(indexAggregates)};
mockUrls[`${rootUrl}/covered_rules.json`] = {json:
{'CPP': {'S1000': 'ver1', 'S987': 'ver2', 'S3457': 'ver1'},
'C': {'S1000': 'c1', 'S234': {'since': 'c2', 'until': 'c3'}},
'PY': {'S3457': {'since': 'p2', 'until': 'p3'}}}
};
return mockUrls;
}
let fetchMocker = fetchMockObject(genMockUrls());
beforeEach(() => {
jest.spyOn(global, 'fetch').mockImplementation(fetchMocker.mock as jest.Mocked<typeof fetch>);
});
afterEach(() => {
fetchMocker.reset();
global.fetch.mockClear();
});
async function renderDefaultSearchPageWithHistory() {
const history = createMemoryHistory();
history.push('/rspec/#/rspec/');
const renderResult = render(<Router history={history}><SearchPage /></Router>);
// Finish rendering after fetching all the data
await waitFor(() => fetchMocker.finished());
return {renderResult, history};
}
async function renderDefaultSearchPage() {
const { renderResult } = await renderDefaultSearchPageWithHistory();
expect(renderResult.queryByTestId('search-hit-S1000')).not.toBeNull();
expect(renderResult.queryByText(/rules found: 5/i)).not.toBeNull();
return renderResult;
}
test('renders the list of all rules', async () => {
const { findByText, asFragment } = await renderDefaultSearchPage();
await findByText(/rules found/i);
expect(asFragment()).toMatchSnapshot();
});
test('narrows search by title', async () => {
const { queryByText, queryByTestId, getByRole } = await renderDefaultSearchPage();
// Enter a search query
const searchBox = getByRole('textbox');
fireEvent.change(searchBox, { target: { value: 'should not be used' } });
expect(queryByTestId('search-hit-S987')).not.toBeNull();
expect(queryByTestId('search-hit-S1000')).toBeNull();
expect(queryByTestId('search-hit-S1007')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).not.toBeNull();
expect(queryByText(/rules found: 3/i)).not.toBeNull();
});
test('on enter navigates to the ruleid', async () => {
const { renderResult: { getByRole }, history } = await renderDefaultSearchPageWithHistory();
// Enter a search query
const searchBox = getByRole('textbox');
fireEvent.change(searchBox, { target: { value: 'S1000' } });
fireEvent.keyUp(searchBox, { key: 'Enter', code: 'Enter', charCode: 13});
await waitFor(() => fetchMocker.finished());
expect(history.entries[history.entries.length - 1].pathname).toBe('/rspec/S1000');
});
test('on enter does not navigate to the wrong ruleid', async () => {
const { renderResult: { getByRole }, history } = await renderDefaultSearchPageWithHistory();
// Enter a search query
const searchBox = getByRole('textbox');
fireEvent.change(searchBox, { target: { value: 'S10000' } });
fireEvent.keyUp(searchBox, { key: 'Enter', code: 'Enter', charCode: 13});
await waitFor(() => fetchMocker.finished());
expect(history.entries[history.entries.length - 1].pathname).toBe('/rspec/');
});
test('does nothing on keyup other than enter', async () => {
const { renderResult: { getByRole }, history } = await renderDefaultSearchPageWithHistory();
// Enter a search query
const searchBox = getByRole('textbox');
fireEvent.change(searchBox, { target: { value: 'S1000' } });
fireEvent.keyUp(searchBox, { key: 'A', code: 'KeyA'});
expect(history.entries[history.entries.length - 1].pathname).toBe('/rspec/');
});
test('on enter navigates to the singular result', async () => {
const { renderResult: {queryByText, getByRole}, history } = await renderDefaultSearchPageWithHistory();
// Enter a search query
const searchBox = getByRole('textbox');
fireEvent.change(searchBox, { target: { value: 'rather validated compiler' } });
expect(queryByText(/rules found: 1/i)).not.toBeNull();
fireEvent.keyUp(searchBox, { key: 'Enter', code: 'Enter', charCode: 13});
await waitFor(() => fetchMocker.finished());
expect(history.entries[history.entries.length - 1].pathname).toBe('/rspec/S3457');
});
test('shows the exact match first', async () => {
const { queryByText, queryByTestId, getAllByTestId, getByRole } = await renderDefaultSearchPage();
// Search for S1000
const searchBox = getByRole('textbox');
fireEvent.change(searchBox, { target: { value: 'S1000' } });
expect(queryByText(/rules found: 2/i)).not.toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S987')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).toBeNull();
// Make sure S1000 comes before S987
let hitsS1000 = getAllByTestId(/search-hit/i).map((hit) => hit.getAttribute('data-testid'));
expect(hitsS1000.indexOf('search-hit-S1000')).toBeLessThan(hitsS1000.indexOf('search-hit-S987'));
// Search for S987
fireEvent.change(searchBox, { target: { value: 'S987' } });
expect(queryByText(/rules found: 2/i)).not.toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S987')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).toBeNull();
// Make sure S987 comes before S1000
let hitsS987 = getAllByTestId(/search-hit/i).map((hit) => hit.getAttribute('data-testid'));
expect(hitsS987.indexOf('search-hit-S987')).toBeLessThan(hitsS987.indexOf('search-hit-S1000'));
});
test('narrows search by rule type', async () => {
const { queryByText, queryByTestId, getByRole, getByTestId } = await renderDefaultSearchPage();
// Select the rule type
fireEvent.mouseDown(within(getByTestId('rule-type')).getByRole('button'));
const listbox = within(getByRole('listbox'));
fireEvent.click(listbox.getByTestId('rule-type-CODE_SMELL'));
expect(queryByTestId('search-hit-S987')).toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S1007')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).not.toBeNull();
expect(queryByText(/rules found: 3/i)).not.toBeNull();
});
test('narrows search by rule tags', async () => {
const { queryByText, queryByTestId, getByRole, getByTestId } = await renderDefaultSearchPage();
// Select the 'clumsy' tag
fireEvent.mouseDown(within(getByTestId('rule-tags')).getByRole('button'));
const listbox = within(getByRole('listbox'));
fireEvent.click(listbox.getByTestId('rule-tag-clumsy'));
expect(queryByText(/rules found: 2/i)).not.toBeNull();
expect(queryByTestId('search-hit-S987')).toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).not.toBeNull();
// Also select the 'pitfall' tag
fireEvent.click(listbox.getByTestId('rule-tag-pitfall'));
expect(queryByText(/rules found: 1/i)).not.toBeNull();
expect(queryByTestId('search-hit-S987')).toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).toBeNull();
});
test('narrows search by language', async () => {
const { queryByText, queryByTestId, getByRole, getByTestId } = await renderDefaultSearchPage();
// Select the cfamily language, should keep all the rules: they all are specified for cfamily
fireEvent.mouseDown(within(getByTestId('rule-language')).getByRole('button'));
const listbox = within(getByRole('listbox'));
fireEvent.click(listbox.getByTestId('rule-language-cfamily'));
expect(queryByText(/rules found: 4/i)).not.toBeNull();
expect(queryByTestId('search-hit-S987')).not.toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).not.toBeNull();
// Select the java language: should keep only S3457
fireEvent.click(listbox.getByTestId('rule-language-java'));
expect(queryByText(/rules found: 2/i)).not.toBeNull();
expect(queryByTestId('search-hit-S987')).toBeNull();
expect(queryByTestId('search-hit-S1000')).toBeNull();
expect(queryByTestId('search-hit-S3457')).not.toBeNull();
});
test('narrows search by quality profile', async () => {
const { queryByText, queryByTestId, getByRole, getByTestId } = await renderDefaultSearchPage();
// Select Sonar way profile - S1000, S1007, S3457 and S3649 are in this profile
fireEvent.mouseDown(within(getByTestId('rule-default-quality-profile')).getByRole('button'));
const listbox = within(getByRole('listbox'));
fireEvent.click(listbox.getByTestId('rule-qual-profile-Sonar way'));
expect(queryByText(/rules found: 3/i)).not.toBeNull();
expect(queryByTestId('search-hit-S987')).toBeNull();
expect(queryByTestId('search-hit-S1000')).not.toBeNull();
expect(queryByTestId('search-hit-S3457')).not.toBeNull();
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,866 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders the list of all rules 1`] = `
<DocumentFragment>
<div
class="makeStyles-root-1"
>
<div
class="makeStyles-searchBar-3"
>
<div
class="MuiContainer-root MuiContainer-maxWidthMd"
>
<div
class="MuiGrid-root MuiGrid-container"
>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined Mui-focused Mui-focused"
data-shrink="true"
for="title-query"
id="title-query-label"
>
Rule Title and Description
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth Mui-focused Mui-focused MuiInputBase-formControl"
>
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input"
id="title-query"
placeholder="Search in rule titles and descriptions"
type="text"
value=""
/>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-8 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-10 PrivateNotchedOutline-legendNotched-11"
>
<span>
Rule Title and Description
</span>
</legend>
</fieldset>
</div>
</div>
</div>
<div
class="MuiGrid-root makeStyles-fullWidth-7 MuiGrid-container MuiGrid-spacing-xs-3 MuiGrid-item MuiGrid-grid-xs-12"
>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-3"
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
data-testid="rule-type"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-marginDense MuiInputLabel-outlined MuiFormLabel-filled"
data-shrink="true"
>
Rule type
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
>
<div
aria-haspopup="listbox"
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
role="button"
tabindex="0"
>
Any
</div>
<input
aria-hidden="true"
class="MuiSelect-nativeInput"
tabindex="-1"
value="ANY"
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-8 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-10 PrivateNotchedOutline-legendNotched-11"
>
<span>
Rule type
</span>
</legend>
</fieldset>
</div>
</div>
</div>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-5"
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
data-testid="rule-tags"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-marginDense MuiInputLabel-outlined"
data-shrink="false"
>
Rule Tags
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
>
<div
aria-haspopup="listbox"
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
role="button"
tabindex="0"
>
<span>
</span>
</div>
<input
aria-hidden="true"
class="MuiSelect-nativeInput"
tabindex="-1"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-8 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-10"
>
<span>
Rule Tags
</span>
</legend>
</fieldset>
</div>
</div>
</div>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-4"
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
data-testid="rule-language"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-marginDense MuiInputLabel-outlined MuiFormLabel-filled"
data-shrink="true"
>
Language
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
>
<div
aria-haspopup="listbox"
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
role="button"
tabindex="0"
>
Any
</div>
<input
aria-hidden="true"
class="MuiSelect-nativeInput"
tabindex="-1"
value="ANY"
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-8 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-10 PrivateNotchedOutline-legendNotched-11"
>
<span>
Language
</span>
</legend>
</fieldset>
</div>
</div>
</div>
</div>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
>
<div
class="MuiFormControl-root MuiTextField-root MuiFormControl-marginNormal MuiFormControl-fullWidth"
data-testid="rule-default-quality-profile"
>
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-marginDense MuiInputLabel-outlined"
data-shrink="false"
>
Default Quality Profiles
</label>
<div
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-marginDense MuiOutlinedInput-marginDense"
>
<div
aria-haspopup="listbox"
class="MuiSelect-root MuiSelect-select MuiSelect-selectMenu MuiSelect-outlined MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputMarginDense MuiOutlinedInput-inputMarginDense"
role="button"
tabindex="0"
>
<span>
</span>
</div>
<input
aria-hidden="true"
class="MuiSelect-nativeInput"
tabindex="-1"
value=""
/>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiSelect-icon MuiSelect-iconOutlined"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M7 10l5 5 5-5z"
/>
</svg>
<fieldset
aria-hidden="true"
class="PrivateNotchedOutline-root-8 MuiOutlinedInput-notchedOutline"
>
<legend
class="PrivateNotchedOutline-legendLabelled-10"
>
<span>
Default Quality Profiles
</span>
</legend>
</fieldset>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="makeStyles-searchResults-4"
>
<div
class="MuiContainer-root MuiContainer-maxWidthMd"
>
<div
class="MuiGrid-root MuiGrid-container MuiGrid-spacing-xs-3"
>
<div
class="MuiGrid-root MuiGrid-item MuiGrid-grid-xs-12"
>
<div
class="MuiBox-root MuiBox-root-12 makeStyles-topRow-5"
>
<div
class="MuiBox-root MuiBox-root-13 makeStyles-resultsCount-6"
>
<h6
class="MuiTypography-root MuiTypography-subtitle1"
>
Number of rules found: 5
</h6>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-14 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-15 MuiPaper-outlined MuiPaper-rounded"
>
<div
class="MuiCardContent-root"
>
<h5
class="MuiTypography-root makeStyles-ruleid-16 MuiTypography-h5 MuiTypography-gutterBottom"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
data-testid="search-hit-S1000"
href="/S1000"
>
<div>
Rule S1000
</div>
</a>
</h5>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
Header files should not contain unnamed namespaces
</p>
<div
class="MuiTableContainer-root"
>
<table
class="MuiTable-root"
>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-coveredTitle-24 MuiTableCell-body"
>
Covered
</td>
<td
class="MuiTableCell-root makeStyles-coveredMarker-25 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S1000/cfamily"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-coveredLanguageChip-19 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
cfamily
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-34 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-15 MuiPaper-outlined MuiPaper-rounded"
>
<div
class="MuiCardContent-root"
>
<h5
class="MuiTypography-root makeStyles-ruleid-16 makeStyles-avoid-17 MuiTypography-h5 MuiTypography-gutterBottom"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
data-testid="search-hit-S1007"
href="/S1007"
>
<div>
Rule S1007
</div>
</a>
</h5>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
When the absolute positioning of bits representing a bit-field is required, then the behaviour and packing of bit-fields shall be documented
</p>
<div
class="MuiTableContainer-root"
>
<table
class="MuiTable-root"
>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-closedTitle-32 MuiTableCell-body"
>
Closed
</td>
<td
class="MuiTableCell-root makeStyles-closedMarker-33 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S1007/cfamily"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-closedLanguageChip-23 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
cfamily
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-35 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-15 MuiPaper-outlined MuiPaper-rounded"
>
<div
class="MuiCardContent-root"
>
<h5
class="MuiTypography-root makeStyles-ruleid-16 MuiTypography-h5 MuiTypography-gutterBottom"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
data-testid="search-hit-S3457"
href="/S3457"
>
<div>
Rule S3457
</div>
</a>
</h5>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
Composite format strings should be used correctly
</p>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
Printf-style format strings should be used correctly
</p>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
String formatting should be used correctly
</p>
<div
class="MuiTableContainer-root"
>
<table
class="MuiTable-root"
>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-coveredTitle-24 MuiTableCell-body"
>
Covered
</td>
<td
class="MuiTableCell-root makeStyles-coveredMarker-25 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/cfamily"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-coveredLanguageChip-19 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
cfamily
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-targetedTitle-26 MuiTableCell-body"
>
Targeted
</td>
<td
class="MuiTableCell-root makeStyles-targetedMarker-27 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/csharp"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-20 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-outlined MuiChip-outlinedPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
csharp
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-removedTitle-28 MuiTableCell-body"
>
Removed
</td>
<td
class="MuiTableCell-root makeStyles-removedMarker-29 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/python"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-removedLanguageChip-21 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
python
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-closedTitle-32 MuiTableCell-body"
>
Closed
</td>
<td
class="MuiTableCell-root makeStyles-closedMarker-33 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3457/java"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-closedLanguageChip-23 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
java
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-36 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-15 MuiPaper-outlined MuiPaper-rounded"
>
<div
class="MuiCardContent-root"
>
<h5
class="MuiTypography-root makeStyles-ruleid-16 MuiTypography-h5 MuiTypography-gutterBottom"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
data-testid="search-hit-S3649"
href="/S3649"
>
<div>
Rule S3649
</div>
</a>
</h5>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
Database queries should not be vulnerable to injection attacks
</p>
<div
class="MuiTableContainer-root"
>
<table
class="MuiTable-root"
>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-targetedTitle-26 MuiTableCell-body"
>
Targeted
</td>
<td
class="MuiTableCell-root makeStyles-targetedMarker-27 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S3649/java"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-targetedLanguageChip-20 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-outlined MuiChip-outlinedPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
java
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div
class="MuiBox-root MuiBox-root-37 makeStyles-searchHitBox-2"
>
<div
class="MuiPaper-root MuiCard-root makeStyles-searchHit-15 MuiPaper-outlined MuiPaper-rounded"
>
<div
class="MuiCardContent-root"
>
<h5
class="MuiTypography-root makeStyles-ruleid-16 MuiTypography-h5 MuiTypography-gutterBottom"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
data-testid="search-hit-S987"
href="/S987"
>
<div>
Rule S987
</div>
</a>
</h5>
<p
class="MuiTypography-root MuiTypography-body1 MuiTypography-gutterBottom"
>
"&lt;signal.h&gt;" should not be used
</p>
<div
class="MuiTableContainer-root"
>
<table
class="MuiTable-root"
>
<tbody
class="MuiTableBody-root"
>
<tr
class="MuiTableRow-root"
>
<td
class="MuiTableCell-root makeStyles-coveredTitle-24 MuiTableCell-body"
>
Covered
</td>
<td
class="MuiTableCell-root makeStyles-coveredMarker-25 MuiTableCell-body"
>
<a
class="MuiTypography-root MuiLink-root MuiLink-underlineHover MuiTypography-colorPrimary"
href="/S987/cfamily"
style="text-decoration: none;"
>
<div
class="MuiButtonBase-root MuiChip-root makeStyles-coveredLanguageChip-19 MuiChip-colorPrimary MuiChip-clickableColorPrimary MuiChip-clickable"
role="button"
tabindex="0"
>
<span
class="MuiChip-label"
>
cfamily
</span>
<span
class="MuiTouchRipple-root"
/>
</div>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<nav
aria-label="pagination navigation"
class="MuiPagination-root"
>
<ul
class="MuiPagination-ul"
>
<li>
<button
aria-label="Go to previous page"
class="MuiButtonBase-root MuiPaginationItem-root MuiPaginationItem-page Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiPaginationItem-icon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"
/>
</svg>
</button>
</li>
<li>
<button
aria-current="true"
aria-label="page 1"
class="MuiButtonBase-root MuiPaginationItem-root MuiPaginationItem-page Mui-selected"
tabindex="0"
type="button"
>
1
<span
class="MuiTouchRipple-root"
/>
</button>
</li>
<li>
<button
aria-label="Go to next page"
class="MuiButtonBase-root MuiPaginationItem-root MuiPaginationItem-page Mui-disabled Mui-disabled"
disabled=""
tabindex="-1"
type="button"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root MuiPaginationItem-icon"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"
/>
</svg>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;

View File

@ -0,0 +1,226 @@
import fs from 'fs';
import path from 'path';
import { generateOneRuleDescription, generateRulesDescription } from '../description';
import { withTestDir, createFiles } from '../testutils';
describe('description generation', () => {
test('generates html from asciidoc', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/rule.adoc': 'Generic content',
'S100/java/rule.adoc': `
include::../rule.adoc[]
Specific content
== Test various forms of auto-link for RSPEC. S100
* See S100, S101,S102.
* See RSPEC-101
* But not S103badref.
* This is a code literal \`+S234+\` but this isn't S567.
\\https://sonarsource.github.io/rspec/#/rspec/S100
https://sonarsource.github.io/rspec/#/rspec/S100/cfamily
https://sonarsource.github.io/rspec/#/rspec/S100/cfamily[]
[source,cpp]
----
int foo() {
// No auto-links in code!
auto S100 = 100;
auto U100 = 100u;
return S100 + U100;
}
----
After snippet, See S100.
[source,cpp]
----
int goo() {
// No auto-links in code!
// S100
}
----
more ref: RSPEC-200.
`,
'S101/rule.adoc': 'Generic content',
'S101/java/rule.adoc': `
include::../rule.adoc[]
Specific content
See S100.
`,
'S501/rule.adoc': 'Generic content, no active language',
});
return withTestDir(async (dstPath) => {
generateRulesDescription(srcPath, dstPath);
const s100Java = path.join(dstPath, 'S100', 'java-description.html');
expect(fs.existsSync(s100Java)).toBeTruthy();
const htmlS100Java = fs.readFileSync(s100Java);
expect(htmlS100Java.toString()).toMatchInlineSnapshot(`
"<div class=\\"sect1\\">
<h2 id=\\"_description\\">Description</h2>
<div class=\\"sectionbody\\">
<div class=\\"paragraph\\">
<p>Generic content
Specific content</p>
</div>
</div>
</div>
<div class=\\"sect1\\">
<h2 id=\\"_test_various_forms_of_auto_link_for_rspec_s100\\">Test various forms of auto-link for RSPEC. <a data-rspec-id=\\"S100\\" class=\\"rspec-auto-link\\">S100</a></h2>
<div class=\\"sectionbody\\">
<div class=\\"ulist\\">
<ul>
<li>
<p>See <a data-rspec-id=\\"S100\\" class=\\"rspec-auto-link\\">S100</a>, <a data-rspec-id=\\"S101\\" class=\\"rspec-auto-link\\">S101</a>,<a data-rspec-id=\\"S102\\" class=\\"rspec-auto-link\\">S102</a>.</p>
</li>
<li>
<p>See <a data-rspec-id=\\"S101\\" class=\\"rspec-auto-link\\">RSPEC-101</a></p>
</li>
<li>
<p>But not S103badref.</p>
</li>
<li>
<p>This is a code literal <code>S234</code> but this isn&#8217;t <a data-rspec-id=\\"S567\\" class=\\"rspec-auto-link\\">S567</a>.</p>
</li>
</ul>
</div>
<div class=\\"paragraph\\">
<p>https://sonarsource.github.io/rspec/#/rspec/S100</p>
</div>
<div class=\\"paragraph\\">
<p><a href=\\"https://sonarsource.github.io/rspec/#/rspec/S100/cfamily\\" class=\\"bare\\">https://sonarsource.github.io/rspec/#/rspec/S100/cfamily</a></p>
</div>
<div class=\\"paragraph\\">
<p><a href=\\"https://sonarsource.github.io/rspec/#/rspec/S100/cfamily\\" class=\\"bare\\">https://sonarsource.github.io/rspec/#/rspec/S100/cfamily</a></p>
</div>
<div class=\\"listingblock\\">
<div class=\\"content\\">
<pre class=\\"highlight\\"><code class=\\"language-cpp\\" data-lang=\\"cpp\\">int foo() {
// No auto-links in code!
auto S100 = 100;
auto U100 = 100u;
return S100 + U100;
}</code></pre>
</div>
</div>
<div class=\\"paragraph\\">
<p>After snippet, See <a data-rspec-id=\\"S100\\" class=\\"rspec-auto-link\\">S100</a>.</p>
</div>
<div class=\\"listingblock\\">
<div class=\\"content\\">
<pre class=\\"highlight\\"><code class=\\"language-cpp\\" data-lang=\\"cpp\\">int goo() {
// No auto-links in code!
// S100
}</code></pre>
</div>
</div>
<div class=\\"paragraph\\">
<p>more ref: <a data-rspec-id=\\"S200\\" class=\\"rspec-auto-link\\">RSPEC-200</a>.</p>
</div>
</div>
</div>"
`);
const s101Java = path.join(dstPath, 'S101', 'java-description.html');
expect(fs.existsSync(s101Java)).toBeTruthy();
const htmlS101Java = fs.readFileSync(s101Java);
expect(htmlS101Java.toString()).toMatchInlineSnapshot(`
"<div class=\\"sect1\\">
<h2 id=\\"_description\\">Description</h2>
<div class=\\"sectionbody\\">
<div class=\\"paragraph\\">
<p>Generic content
Specific content
See <a data-rspec-id=\\"S100\\" class=\\"rspec-auto-link\\">S100</a>.</p>
</div>
</div>
</div>"
`);
const s501Default = path.join(dstPath, 'S501', 'default-description.html');
expect(fs.existsSync(s501Default)).toBeTruthy();
const htmlS501Default = fs.readFileSync(s501Default);
expect(htmlS501Default.toString()).toMatchInlineSnapshot(`
"<div class=\\"sect1\\">
<h2 id=\\"_description\\">Description</h2>
<div class=\\"sectionbody\\">
<div class=\\"paragraph\\">
<p>Generic content, no active language</p>
</div>
</div>
</div>"
`);
});
});
});
function normalizeString(str: string) {
// Ignore \n\r vs \n differences, and ignore extra whitespace.
return str.replace(/\r\n/g, '\n').trimEnd();
}
expect.extend({
toBeSameAsFile(received: string, expectedPath: string) {
if (!fs.existsSync(expectedPath)) {
return {
message: () => `File ${expectedPath} was not found.`,
pass: false
};
}
const expected = fs.readFileSync(expectedPath).toString();
if (normalizeString(expected) === normalizeString(received)) {
return {
// This message is used in case of test negation `expect(a).not.toBeSameAsFile(f)`
message: () => `Identity check failed on ${expectedPath}.\nExpected:\n${expected}\n\nReceived:\n${received}`,
pass: true
};
} else {
const receivedPath = path.join(path.dirname(expectedPath), 'received-' + path.basename(expectedPath));
fs.writeFileSync(receivedPath, received);
return {
message: () => `Identity check failed on ${expectedPath}.\nReceived file saved in ${receivedPath}`,
pass: false
};
}
}
});
test('generates description for active rules', () => {
return withTestDir(async (dstPath) => {
generateRulesDescription(path.join(__dirname, 'resources', 'rules'), dstPath);
const rules = fs.readdirSync(dstPath);
expect(rules.length).toEqual(5);
let treated = 0;
rules.forEach(ruleDir => {
const languages = fs.readdirSync(`${dstPath}/${ruleDir}`);
expect(languages.length).toBeGreaterThanOrEqual(1);
languages.forEach(file => {
const actual = fs.readFileSync(`${dstPath}/${ruleDir}/${file}`).toString();
const expectedPath = path.join(__dirname, 'resources', 'metadata', ruleDir, file);
expect(actual).toBeSameAsFile(expectedPath);
treated++;
});
});
expect(treated).toBe(13);
});
});
test('Generate one rule description for a rule with a "common" directory', () => {
return withTestDir(async (dstPath) => {
generateOneRuleDescription(path.join(__dirname, 'resources', 'rules', 'S3649'), dstPath);
const s3649Java = path.join(dstPath, 'java-description.html');
expect(fs.existsSync(s3649Java)).toBeTruthy();
const actual = fs.readFileSync(s3649Java).toString();
const expectedPath = path.join(__dirname, 'resources', 'metadata', 'S3649', 'java-description.html');
expect(actual).toBeSameAsFile(expectedPath);
});
})
});

View File

@ -0,0 +1,311 @@
import fs from 'fs';
import path from 'path';
import { generateOneRuleMetadata, generateRulesMetadata } from '../metadata';
import { withTestDir, createFiles } from '../testutils';
describe('metadata generation', () => {
test('language specific metadata overrides generic metadata', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/metadata.json': JSON.stringify({
title: 'Rule S100',
tags: ['confusing']
}),
'S100/java/metadata.json': JSON.stringify({
title: 'Java Rule S100'
}),
'S100/python/metadata.json': JSON.stringify({
tags: ['pep8']
}),
});
return withTestDir(async (dstPath) => {
generateRulesMetadata(srcPath, dstPath);
const javaStrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const javaMetadata = JSON.parse(javaStrMetadata.toString());
expect(javaMetadata).toMatchObject({
title: 'Java Rule S100',
tags: ['confusing']
});
const pythonStrMetadata = fs.readFileSync(`${dstPath}/S100/python-metadata.json`);
const pythonMetadata = JSON.parse(pythonStrMetadata.toString());
expect(pythonMetadata).toMatchObject({
title: 'Rule S100',
tags: ['pep8']
});
});
});
});
test('check status computation', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/metadata.json': JSON.stringify({
title: 'Rule S100',
status: 'ready'
}),
'S100/java/metadata.json': JSON.stringify({
title: 'Java Rule S100'
}),
'S100/python/metadata.json': JSON.stringify({
status: 'closed'
}),
});
return withTestDir(async (dstPath) => {
generateRulesMetadata(srcPath, dstPath);
const javaStrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const pythonStrMetadata = fs.readFileSync(`${dstPath}/S100/python-metadata.json`);
const javaMetadata = JSON.parse(javaStrMetadata.toString());
const pythonMetadata = JSON.parse(pythonStrMetadata.toString());
expect(pythonMetadata).toMatchObject({
title: 'Rule S100',
languagesSupport: [
{name: 'java', status: 'ready'},
{name: 'python', status: 'closed'}
]
});
expect(javaMetadata.languagesSupport).toStrictEqual(pythonMetadata.languagesSupport);
});
});
});
test('computes rule types correctly', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/metadata.json': JSON.stringify({
title: 'Rule S100',
type: 'CODE_SMELL',
}),
'S100/java/metadata.json': JSON.stringify({
title: 'Java Rule S100',
}),
'S100/python/metadata.json': JSON.stringify({
type: 'CODE_SMELL',
}),
'S100/cfamily/metadata.json': JSON.stringify({
type: 'BUG',
}),
});
return withTestDir(async (dstPath) => {
generateRulesMetadata(srcPath, dstPath);
const javaStrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const pythonStrMetadata = fs.readFileSync(`${dstPath}/S100/python-metadata.json`);
const cfamilyStrMetadata = fs.readFileSync(`${dstPath}/S100/cfamily-metadata.json`);
const javaMetadata = JSON.parse(javaStrMetadata.toString());
const pythonMetadata = JSON.parse(pythonStrMetadata.toString());
const cfamilyMetadata = JSON.parse(cfamilyStrMetadata.toString());
expect(javaMetadata).toMatchObject({
title: 'Java Rule S100',
type: 'CODE_SMELL',
});
expect(pythonMetadata).toMatchObject({
title: 'Rule S100',
type: 'CODE_SMELL',
});
expect(cfamilyMetadata).toMatchObject({
title: 'Rule S100',
type: 'BUG',
});
});
});
});
test('generates only requested rules if a list of rule is provided', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/java/metadata.json': JSON.stringify({
title: 'Rule S100'
}),
'S200/java/metadata.json': JSON.stringify({
title: 'Rule S200'
}),
});
return withTestDir(async (dstPath) => {
generateRulesMetadata(srcPath, dstPath, ['S100']);
const s100Exists = fs.existsSync(`${dstPath}/S100/java-metadata.json`);
expect(s100Exists).toBeTruthy();
const s200Exists = fs.existsSync(`${dstPath}/S200/java-metadata.json`);
expect(s200Exists).toBeFalsy();
});
});
});
test('forwards the pr url when provided', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/java/metadata.json': JSON.stringify({
title: 'Rule S100'
}),
'S200/java/metadata.json': JSON.stringify({
title: 'Rule S200'
}),
});
return withTestDir(async (dstPath) => {
generateOneRuleMetadata(path.join(srcPath, 'S100'), path.join(dstPath, 'S100'), 'master');
const s100StrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const s100Metadata = JSON.parse(s100StrMetadata.toString());
expect(Object.keys(s100Metadata)).toContain('branch');
expect(s100Metadata.branch).toEqual('master');
expect(Object.keys(s100Metadata)).not.toContain('prUrl');
generateOneRuleMetadata(path.join(srcPath, 'S200'), path.join(dstPath, 'S200'), 'add-my-rule', 'https://some.pr/url');
const s200StrMetadata = fs.readFileSync(`${dstPath}/S200/java-metadata.json`);
const s200Metadata = JSON.parse(s200StrMetadata.toString());
expect(Object.keys(s200Metadata)).toContain('prUrl');
expect(s200Metadata.branch).toEqual('add-my-rule');
expect(s200Metadata.prUrl).toEqual('https://some.pr/url');
});
});
});
test('generates metadata for active rules', () => {
return withTestDir(async (dstPath) => {
generateRulesMetadata(path.join(__dirname, 'resources', 'rules'), dstPath);
const rules = fs.readdirSync(dstPath);
expect(rules.length).toEqual(5);
let treated = 0;
rules.forEach(ruleDir => {
const languages = fs.readdirSync(`${dstPath}/${ruleDir}`);
expect(languages.length).toBeGreaterThanOrEqual(1);
languages.forEach(file => {
const actual = JSON.parse(fs.readFileSync(`${dstPath}/${ruleDir}/${file}`).toString());
const expectedPath = path.join(__dirname, 'resources', 'metadata', ruleDir, file);
const expected = JSON.parse(fs.readFileSync(expectedPath).toString());
expect(actual).toStrictEqual(expected);
treated++;
})
});
expect(treated).toBe(13);
});
});
test('generates metadata for closed rules', () => {
return withTestDir(srcPath => {
createFiles(srcPath, {
'S01/metadata.json': JSON.stringify({
title: 'Rule is closed and has no language-specific specification',
type: 'CODE_SMELL',
status: 'closed',
sqKey: 'S01',
extra: {
legacyKeys: ['OldS01'],
},
}),
'S02/metadata.json': JSON.stringify({
title: 'Rule is closed and has one closed language-specific specification',
type: 'CODE_SMELL',
status: 'closed',
sqKey: 'S02',
}),
'S02/cfamily/metadata.json': JSON.stringify({
title: 'Language specification is closed',
status: 'closed',
extra: {
legacyKeys: ['OldS02'],
},
}),
});
return withTestDir(async dstPath => {
generateRulesMetadata(srcPath, dstPath);
const rules = fs.readdirSync(dstPath).sort();
expect(rules).toEqual(['S01', 'S02'].sort());
{
const rule = 'S01';
const rulePath = path.join(dstPath, rule);
// Verify that the expected files are generated and no others
const entries = fs.readdirSync(rulePath).sort();
expect(entries).toEqual(['default-metadata.json'].sort());
// Check the top-level metadata
const defaultFile = path.join(rulePath, 'default-metadata.json');
const defaultData = JSON.parse(fs.readFileSync(defaultFile, 'utf8'));
expect(defaultData).toMatchObject({
title: 'Rule is closed and has no language-specific specification',
type: 'CODE_SMELL',
status: 'closed',
languagesSupport: [],
allKeys: ['S01', 'OldS01'],
});
}
{
const rule = 'S02';
const rulePath = path.join(dstPath, rule);
// Verify that the expected files are generated and no others
const entries = fs.readdirSync(rulePath).sort();
expect(entries).toEqual(['default-metadata.json', 'cfamily-metadata.json'].sort());
// Check the top-level metadata
const defaultFile = path.join(rulePath, 'default-metadata.json');
const defaultData = JSON.parse(fs.readFileSync(defaultFile, 'utf8'));
// Generic data is overriden by the first language-specific specification.
expect(defaultData).toMatchObject({
title: 'Language specification is closed',
type: 'CODE_SMELL',
status: 'closed',
languagesSupport: [{ name: 'cfamily', status: 'closed', }],
allKeys: ['S02', 'OldS02'],
});
// Check the language-specific metadata
const cfamilyFile = path.join(rulePath, 'cfamily-metadata.json');
const cfamilyData = JSON.parse(fs.readFileSync(cfamilyFile, 'utf8'));
expect(cfamilyData).toMatchObject({
title: 'Language specification is closed',
type: 'CODE_SMELL',
status: 'closed',
languagesSupport: [{ name: 'cfamily', status: 'closed', }],
allKeys: ['S02', 'OldS02'],
});
}
});
});
});
test('allKeys include the sqKey overridden in all languages', () => {
return withTestDir((srcPath) => {
createFiles(srcPath, {
'S100/metadata.json': JSON.stringify({
sqKey: 'S100',
}),
'S100/java/metadata.json': JSON.stringify({
sqKey: 'S100-Java'
}),
'S100/python/metadata.json': JSON.stringify({
sqKey: 'S100-python'
}),
});
return withTestDir(async (dstPath) => {
generateRulesMetadata(srcPath, dstPath);
const javaStrMetadata = fs.readFileSync(`${dstPath}/S100/java-metadata.json`);
const javaMetadata = JSON.parse(javaStrMetadata.toString());
expect(javaMetadata).toMatchObject({
allKeys: ['S100-Java', 'S100-python', 'S100']
});
const pythonStrMetadata = fs.readFileSync(`${dstPath}/S100/python-metadata.json`);
const pythonMetadata = JSON.parse(pythonStrMetadata.toString());
expect(pythonMetadata).toMatchObject({
allKeys: ['S100-Java', 'S100-python', 'S100']
});
});
});
});
});

View File

@ -0,0 +1,59 @@
import fs from 'fs';
import { process_incomplete_rspecs, PullRequest } from '../pullRequestIndexing';
import Git from 'nodegit';
import 'setimmediate';
jest.mock('@octokit/rest', () => {
return {Octokit: function() {
this.rest = {pulls: {list: jest.fn(() => {
return { data: [
{ title: 'Irrelevant S832' },
{ title: 'Create rule S343: Be friendly',
html_url: 'https://pull.request/url',
head: {ref: 'gh:rspec/branches/friendly-branch'},
number: 42, },
{ title: 'Create rule S01, and friends',
html_url: 'example.com',
head: {ref: 'gh:rspec/branches/some-other-branch'},
number: 1, },
] }
})}};
}};
});
beforeEach(() => {
Git.Clone.clone = jest.fn();
let repo = {
config: () => ({ setString: (name: string, value: string) => {} }),
fetch: (remote: string) => {},
getBranch: (name: string) => {},
checkoutRef: (ref) => {}
};
Git.Clone.clone.mockReturnValueOnce(repo);
jest.spyOn(fs, 'existsSync').mockImplementation((fname) => {
return fname.replace(/\\/g, '/').includes('rules/S');
});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('pull request enumeration', () => {
test('clones repository and lists only relevant PRs', () => {
const nonExistingDir = 'not-existing-directory';
let processedPRs = [];
return process_incomplete_rspecs(nonExistingDir, (srcDir: string, pr: PullRequest) => {
processedPRs.push(pr.pull_id);
}).then(() => {
expect(Git.Clone.clone.mock.calls.length).toBe(1);
expect(Git.Clone.clone.mock.calls[0][1]).toBe(nonExistingDir);
expect(processedPRs).toHaveLength(2);
expect(processedPRs).toContain(1);
expect(processedPRs).toContain(42);
});
});
});

View File

@ -0,0 +1,94 @@
<div class="sect1">
<h2 id="_description">Description</h2>
<div class="sectionbody">
<div class="paragraph">
<p>An unnamed namespace will be unique within each translation unit. Any declarations appearing in an unnamed namespace in a header will refer to a different entity in each translation unit, which is probably not the expected behavior.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_noncompliant_code_example">Noncompliant Code Example</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-cpp" data-lang="cpp">// Header.hpp
namespace // Noncompliant
{
extern int32_t x;
}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-cpp" data-lang="cpp">// File1.cpp
#include "Header.hpp"
namespace
{
int32_t x;
}
void fn_a(void)
{
x = 42;
}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-cpp" data-lang="cpp">// File2.cpp
#include "Header.hpp"
namespace
{
int32_t x; // this is a different x than in File1.cpp
}
void fn_b(void)
{
fn_a(); // Is expected to initialize "x" to 42
if (x == 42) // But does not, as there are 2 distinct "x" variables
{
// I am NOT an auto link in some source code: S987.
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Mentioning <a data-rspec-id="S987" class="rspec-auto-link">S987</a> <a data-rspec-id="S987" class="rspec-auto-link">RSPEC-987</a> here for testing purposes.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_see">See</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>MISRA C&#43;&#43;:2008, 7-3-3 - There shall be no unnamed namespaces in header files.</p>
</li>
<li>
<p><a href="https://wiki.sei.cmu.edu/confluence/x/VXs-BQ">CERT, DCL59-CPP.</a> - Do not define an unnamed namespace in a header file</p>
</li>
</ul>
</div>
<hr>
</div>
</div>
<div class="sect1">
<h2 id="_comments_and_links">Comments And Links</h2>
<div class="sectionbody">
<div class="paragraph">
<p>(visible only on this page)</p>
</div>
<div class="sect2">
<h3 id="_on_8_feb_2018_004304_thomas_epperson_wrote">on 8 Feb 2018, 00:43:04 Thomas Epperson wrote:</h3>
<div class="paragraph">
<p>The implementation incorrectly flags unnamed namespaces in source files. The specs only refer to unnamed namespaces in HEADER files.</p>
</div>
<div class="paragraph">
<p>See reported bugs in https://sonarcloud.io/dashboard?id=uglyoldbob_decompiler%3Arestructure</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,44 @@
{
"title": "Header files should not contain unnamed namespaces",
"type": "CODE_SMELL",
"status": "ready",
"quickfix": "unknown",
"remediation": {
"func": "Constant/Issue",
"constantCost": "1h"
},
"tags": [
"cert",
"misra-c++2008",
"clumsy",
"pitfall"
],
"extra": {
"legacyKeys": [
"UnnamedNamespaceInHeader"
],
"replacementRules": []
},
"defaultSeverity": "Major",
"ruleSpecification": "RSPEC-1000",
"sqKey": "UnnamedNamespaceInHeader",
"scope": "Main",
"securityStandards": {
"CERT": [
"DCL59-CPP."
]
},
"defaultQualityProfiles": [
"Sonar way"
],
"languagesSupport": [
{
"name": "cfamily",
"status": "ready"
}
],
"allKeys": [
"UnnamedNamespaceInHeader"
],
"branch": "master"
}

View File

@ -0,0 +1,94 @@
<div class="sect1">
<h2 id="_description">Description</h2>
<div class="sectionbody">
<div class="paragraph">
<p>An unnamed namespace will be unique within each translation unit. Any declarations appearing in an unnamed namespace in a header will refer to a different entity in each translation unit, which is probably not the expected behavior.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_noncompliant_code_example">Noncompliant Code Example</h2>
<div class="sectionbody">
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-cpp" data-lang="cpp">// Header.hpp
namespace // Noncompliant
{
extern int32_t x;
}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-cpp" data-lang="cpp">// File1.cpp
#include "Header.hpp"
namespace
{
int32_t x;
}
void fn_a(void)
{
x = 42;
}</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlight"><code class="language-cpp" data-lang="cpp">// File2.cpp
#include "Header.hpp"
namespace
{
int32_t x; // this is a different x than in File1.cpp
}
void fn_b(void)
{
fn_a(); // Is expected to initialize "x" to 42
if (x == 42) // But does not, as there are 2 distinct "x" variables
{
// I am NOT an auto link in some source code: S987.
}
}</code></pre>
</div>
</div>
<div class="paragraph">
<p>Mentioning <a data-rspec-id="S987" class="rspec-auto-link">S987</a> <a data-rspec-id="S987" class="rspec-auto-link">RSPEC-987</a> here for testing purposes.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_see">See</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p>MISRA C&#43;&#43;:2008, 7-3-3 - There shall be no unnamed namespaces in header files.</p>
</li>
<li>
<p><a href="https://wiki.sei.cmu.edu/confluence/x/VXs-BQ">CERT, DCL59-CPP.</a> - Do not define an unnamed namespace in a header file</p>
</li>
</ul>
</div>
<hr>
</div>
</div>
<div class="sect1">
<h2 id="_comments_and_links">Comments And Links</h2>
<div class="sectionbody">
<div class="paragraph">
<p>(visible only on this page)</p>
</div>
<div class="sect2">
<h3 id="_on_8_feb_2018_004304_thomas_epperson_wrote">on 8 Feb 2018, 00:43:04 Thomas Epperson wrote:</h3>
<div class="paragraph">
<p>The implementation incorrectly flags unnamed namespaces in source files. The specs only refer to unnamed namespaces in HEADER files.</p>
</div>
<div class="paragraph">
<p>See reported bugs in https://sonarcloud.io/dashboard?id=uglyoldbob_decompiler%3Arestructure</p>
</div>
</div>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More