diff --git a/.github/workflows/generate-code-scanning-query-lists.yml b/.github/workflows/generate-code-scanning-query-lists.yml index bf4aa979fcec..1da11640e0b0 100644 --- a/.github/workflows/generate-code-scanning-query-lists.yml +++ b/.github/workflows/generate-code-scanning-query-lists.yml @@ -19,7 +19,8 @@ on: pull_request: paths: - .github/workflows/generate-code-scanning-query-lists.yml - - src/code-scanning/scripts/generate-code-scanning-query-list.ts + - src/codeql-queries/scripts/generate-code-scanning-query-list.ts + - src/codeql-queries/scripts/generate-code-quality-query-list.ts - .github/actions/install-cocofix/action.yml permissions: @@ -27,7 +28,7 @@ permissions: pull-requests: write jobs: - generate-query-lists: + generate-security-query-lists: if: github.repository == 'github/docs-internal' runs-on: ubuntu-latest steps: @@ -45,6 +46,7 @@ jobs: - name: Get the codeql SHA being synced id: codeql + shell: bash run: | cd codeql OPENAPI_COMMIT_SHA=$(git rev-parse HEAD) @@ -56,12 +58,14 @@ jobs: uses: ./codeql/.github/actions/fetch-codeql - name: Test CodeQL CLI Download + shell: bash run: codeql --version # "Server for running multiple commands while avoiding repeated JVM initialization." # Having started this should speed up the execution of the various # CLI calls of the executable. - name: Start CodeQL CLI server in the background + shell: bash run: | codeql execute cli-server & sleep 3 @@ -71,21 +75,8 @@ jobs: with: token: ${{ secrets.DOCS_BOT_PAT_BASE }} - - name: Lint the code (eslint) - if: ${{ github.event_name == 'pull_request' }} - env: - PATH: '$PATH:${{ github.workspace }}/node_modules/.bin' - run: | - eslint --no-ignore src/code-scanning/scripts/generate-code-scanning-query-list.ts - - - name: Lint the code (tsc) - if: ${{ github.event_name == 'pull_request' }} - env: - PATH: '$PATH:${{ github.workspace }}/node_modules/.bin' - run: | - tsc --noEmit --project src/code-scanning/scripts/tsconfig.json - - - name: Build code scanning query list + - name: Build code scanning security query lists + shell: bash run: | for lang in "actions" "cpp" "csharp" "go" "java" "javascript" "python" "ruby" "rust" "swift"; do echo "Generating code scanning query list for $lang" @@ -97,7 +88,112 @@ jobs: $lang done + - name: Upload security query lists + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: security-query-lists + path: data/reusables/code-scanning/codeql-query-tables/ + + generate-quality-query-lists: + if: github.repository == 'github/docs-internal' + runs-on: ubuntu-latest + steps: + - name: Checkout repository code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - uses: ./.github/actions/node-npm-setup + + - name: Checkout codeql repo + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: github/codeql + path: codeql + ref: ${{ inputs.SOURCE_BRANCH || 'main' }} + + - name: Get the codeql SHA being synced + id: codeql + shell: bash + run: | + cd codeql + OPENAPI_COMMIT_SHA=$(git rev-parse HEAD) + echo "OPENAPI_COMMIT_SHA=$OPENAPI_COMMIT_SHA" >> $GITHUB_OUTPUT + echo "Copied files from github/codeql repo. Commit SHA: $OPENAPI_COMMIT_SHA" + + - name: Download CodeQL CLI + # Look under the `codeql` directory, as this is where we checked out the `github/codeql` repo + uses: ./codeql/.github/actions/fetch-codeql + + - name: Test CodeQL CLI Download + shell: bash + run: codeql --version + + # "Server for running multiple commands while avoiding repeated JVM initialization." + # Having started this should speed up the execution of the various + # CLI calls of the executable. + - name: Start CodeQL CLI server in the background + shell: bash + run: | + codeql execute cli-server & + sleep 3 + codeql --version + + - name: Build code quality query lists + shell: bash + run: | + for lang in "csharp" "go" "java" "javascript" "python" "ruby"; do + echo "Generating code quality query list for $lang" + npm run generate-code-quality-query-list -- \ + --verbose \ + --codeql-path codeql \ + --codeql-dir codeql \ + -o data/reusables/code-quality/codeql-query-tables/$lang.md \ + $lang + done + + - name: Upload quality query lists + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: quality-query-lists + path: data/reusables/code-quality/codeql-query-tables/ + + create-pull-request: + if: github.repository == 'github/docs-internal' + runs-on: ubuntu-latest + needs: [generate-security-query-lists, generate-quality-query-lists] + steps: + - name: Checkout repository code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Checkout codeql repo + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + repository: github/codeql + path: codeql + ref: ${{ inputs.SOURCE_BRANCH || 'main' }} + + - name: Get the codeql SHA being synced + id: codeql + shell: bash + run: | + cd codeql + OPENAPI_COMMIT_SHA=$(git rev-parse HEAD) + echo "OPENAPI_COMMIT_SHA=$OPENAPI_COMMIT_SHA" >> $GITHUB_OUTPUT + echo "Copied files from github/codeql repo. Commit SHA: $OPENAPI_COMMIT_SHA" + + - name: Download security query lists + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + name: security-query-lists + path: data/reusables/code-scanning/codeql-query-tables/ + + - name: Download quality query lists + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + name: quality-query-lists + path: data/reusables/code-quality/codeql-query-tables/ + - name: Insight into diff + shell: bash run: | git diff @@ -105,6 +201,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} DRY_RUN: ${{ github.event_name == 'pull_request'}} + shell: bash run: | # When we started, we downloaded the CodeQL CLI here in this workflow. @@ -145,6 +242,7 @@ jobs: fi git add data/reusables/code-scanning/codeql-query-tables + git add data/reusables/code-quality/codeql-query-tables git commit -m "Update CodeQL query tables" git push -u origin $branchname @@ -157,6 +255,6 @@ jobs: No action is required from the first responder for the Docs content team. This PR is automatically added to the Docs content review board. Any writer can review this by checking that the PR looks sensible. If CI does not pass or other problems arise, contact #docs-engineering on slack. - - + + When the DRI for the CodeQL CLI release is ready to publish, they will ask us to merge this PR in #docs-content.' diff --git a/content/code-security/code-quality/reference/codeql-detection.md b/content/code-security/code-quality/reference/codeql-detection.md index a96d3d27e3c7..96158faa6090 100644 --- a/content/code-security/code-quality/reference/codeql-detection.md +++ b/content/code-security/code-quality/reference/codeql-detection.md @@ -1,6 +1,7 @@ --- -title: CodeQL detection of code quality problems -shortTitle: CodeQL detection +title: CodeQL-powered analysis for Code Quality +shortTitle: CodeQL analysis +allowTitleToDifferFromFilename: true intro: 'Information on how CodeQL-powered analysis for {% data variables.product.prodname_code_quality_short %} works, the workflow used, and the status checks reported on pull requests.' versions: feature: code-quality @@ -11,11 +12,30 @@ contentType: reference {% data reusables.code-quality.code-quality-preview-note %} -## {% data variables.product.prodname_codeql %} detection +## {% data variables.product.prodname_codeql %}-powered analysis -{% data variables.product.prodname_code_quality_short %} performs rule-based analysis of pull requests and your default branch using {% data variables.product.prodname_codeql %}. Each rule is written as a query in {% data variables.product.prodname_codeql %} and then run using {% data variables.product.prodname_actions %}. +{% data variables.product.prodname_code_quality_short %} uses {% data variables.product.prodname_codeql %} to perform rule-based analysis of pull requests and your default branch. -The rules are continually refined by both {% data variables.product.github %} and open source developers. See [https://github.com/github/codeql](https://github.com/github/codeql). +* Findings for your **default branch** appear under the "{% data variables.code-quality.all_findings %}" dashboard under your repository's Security tab. + +* Findings **on pull requests** appear as comments made by `{% data variables.code-quality.pr_commenter %}`. + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +### Query lists for supported languages + +Each {% data variables.product.prodname_code_quality_short %} rule is written as a query in {% data variables.product.prodname_codeql %} and then run using {% data variables.product.prodname_actions %}. + +The rules are continually refined by both {% data variables.product.github %} and open source developers. + +* [AUTOTITLE](/code-security/code-quality/reference/codeql-queries/csharp-queries) +* [AUTOTITLE](/code-security/code-quality/reference/codeql-queries/go-queries) +* [AUTOTITLE](/code-security/code-quality/reference/codeql-queries/java-queries) +* [AUTOTITLE](/code-security/code-quality/reference/codeql-queries/javascript-queries) +* [AUTOTITLE](/code-security/code-quality/reference/codeql-queries/python-queries) +* [AUTOTITLE](/code-security/code-quality/reference/codeql-queries/ruby-queries) + +For more information about the {% data variables.product.prodname_codeql %} project, see [https://codeql.github.com/](https://codeql.github.com/). ## Workflow used for code quality analysis diff --git a/content/code-security/code-quality/reference/codeql-queries/csharp-queries.md b/content/code-security/code-quality/reference/codeql-queries/csharp-queries.md new file mode 100644 index 000000000000..386f44e61bf7 --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/csharp-queries.md @@ -0,0 +1,20 @@ +--- +title: C# CodeQL queries for Code Quality +shortTitle: C# queries +allowTitleToDifferFromFilename: true +intro: 'Explore the queries that {% data variables.product.prodname_codeql %} uses to analyze code quality for code written in C#.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +--- + +{% data variables.product.prodname_code_quality_short %} uses the following {% data variables.product.prodname_codeql %} queries to analyze C# code and detect code quality issues on: + +* Your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* **Pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +{% data reusables.code-quality.codeql-query-tables.csharp %} \ No newline at end of file diff --git a/content/code-security/code-quality/reference/codeql-queries/go-queries.md b/content/code-security/code-quality/reference/codeql-queries/go-queries.md new file mode 100644 index 000000000000..fd142d09ab4f --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/go-queries.md @@ -0,0 +1,19 @@ +--- +title: Go CodeQL queries for Code Quality +shortTitle: Go queries +intro: 'Explore the queries that {% data variables.product.prodname_codeql %} uses to analyze code quality for code written in Go.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +--- + +{% data variables.product.prodname_code_quality_short %} uses the following {% data variables.product.prodname_codeql %} queries to analyze Go code and detect code quality issues on: + +* Your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* **Pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +{% data reusables.code-quality.codeql-query-tables.go %} \ No newline at end of file diff --git a/content/code-security/code-quality/reference/codeql-queries/index.md b/content/code-security/code-quality/reference/codeql-queries/index.md new file mode 100644 index 000000000000..87e6de910bfc --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/index.md @@ -0,0 +1,17 @@ +--- +title: Queries for CodeQL detection +shortTitle: CodeQL queries +intro: 'Explore the {% data variables.product.prodname_codeql %} queries that {% data variables.product.prodname_code_quality_short %} uses to detect code quality issues in supported languages.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +children: + - csharp-queries + - go-queries + - java-queries + - javascript-queries + - python-queries + - ruby-queries +--- \ No newline at end of file diff --git a/content/code-security/code-quality/reference/codeql-queries/java-queries.md b/content/code-security/code-quality/reference/codeql-queries/java-queries.md new file mode 100644 index 000000000000..d0fe33c5d229 --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/java-queries.md @@ -0,0 +1,20 @@ +--- +title: Java CodeQL queries for Code Quality +shortTitle: Java queries +allowTitleToDifferFromFilename: true +intro: 'Explore the queries that {% data variables.product.prodname_codeql %} uses to analyze code quality for code written in Java.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +--- + +{% data variables.product.prodname_code_quality_short %} uses the following {% data variables.product.prodname_codeql %} queries to analyze Java code and detect code quality issues on: + +* Your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* **Pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +{% data reusables.code-quality.codeql-query-tables.java %} diff --git a/content/code-security/code-quality/reference/codeql-queries/javascript-queries.md b/content/code-security/code-quality/reference/codeql-queries/javascript-queries.md new file mode 100644 index 000000000000..1ca4e49cddb8 --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/javascript-queries.md @@ -0,0 +1,20 @@ +--- +title: JavaScript CodeQL queries for Code Quality +shortTitle: JavaScript queries +allowTitleToDifferFromFilename: true +intro: 'Explore the queries that {% data variables.product.prodname_codeql %} uses to analyze code quality for code written in JavaScript.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +--- + +{% data variables.product.prodname_code_quality_short %} uses the following {% data variables.product.prodname_codeql %} queries to analyze JavaScript code and detect code quality issues on: + +* Your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* **Pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +{% data reusables.code-quality.codeql-query-tables.javascript %} diff --git a/content/code-security/code-quality/reference/codeql-queries/python-queries.md b/content/code-security/code-quality/reference/codeql-queries/python-queries.md new file mode 100644 index 000000000000..24784bf558d0 --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/python-queries.md @@ -0,0 +1,20 @@ +--- +title: Python CodeQL queries for Code Quality +shortTitle: Python queries +allowTitleToDifferFromFilename: true +intro: 'Explore the queries that {% data variables.product.prodname_codeql %} uses to analyze code quality for code written in Python.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +--- + +{% data variables.product.prodname_code_quality_short %} uses the following {% data variables.product.prodname_codeql %} queries to analyze Python code and detect code quality issues on: + +* Your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* **Pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +{% data reusables.code-quality.codeql-query-tables.python %} diff --git a/content/code-security/code-quality/reference/codeql-queries/ruby-queries.md b/content/code-security/code-quality/reference/codeql-queries/ruby-queries.md new file mode 100644 index 000000000000..79417da34057 --- /dev/null +++ b/content/code-security/code-quality/reference/codeql-queries/ruby-queries.md @@ -0,0 +1,20 @@ +--- +title: Ruby CodeQL queries for Code Quality +shortTitle: Ruby queries +allowTitleToDifferFromFilename: true +intro: 'Explore the queries that {% data variables.product.prodname_codeql %} uses to analyze code quality for code written in Ruby.' +versions: + feature: code-quality +topics: + - Code Quality +contentType: reference +--- + +{% data variables.product.prodname_code_quality_short %} uses the following {% data variables.product.prodname_codeql %} queries to analyze Ruby code and detect code quality issues on: + +* Your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* **Pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. + +{% data reusables.code-quality.codeql-query-tables.ruby %} diff --git a/content/code-security/code-quality/reference/index.md b/content/code-security/code-quality/reference/index.md index 643f83e7bcfe..188d0f76f4de 100644 --- a/content/code-security/code-quality/reference/index.md +++ b/content/code-security/code-quality/reference/index.md @@ -10,4 +10,5 @@ contentType: reference children: - metrics-and-ratings - codeql-detection + - codeql-queries --- diff --git a/content/repositories/creating-and-managing-repositories/index.md b/content/repositories/creating-and-managing-repositories/index.md index 36516d42b6d1..b5d0753944ff 100644 --- a/content/repositories/creating-and-managing-repositories/index.md +++ b/content/repositories/creating-and-managing-repositories/index.md @@ -27,6 +27,7 @@ children: - /transferring-a-repository - /deleting-a-repository - /restoring-a-deleted-repository + - /viewing-all-your-repositories shortTitle: Create & manage repositories --- diff --git a/content/repositories/creating-and-managing-repositories/viewing-all-your-repositories.md b/content/repositories/creating-and-managing-repositories/viewing-all-your-repositories.md new file mode 100644 index 000000000000..dd7eee192f4b --- /dev/null +++ b/content/repositories/creating-and-managing-repositories/viewing-all-your-repositories.md @@ -0,0 +1,47 @@ +--- +title: Viewing all repositories +intro: 'The repository dashboard lists the repositories you''ve created as well as where you''ve made contributions. You can use search and filters to find the right repositories and create saved views.' +allowTitleToDifferFromFilename: true +versions: + fpt: '*' + ghec: '*' + ghes: '>=3.22' +topics: + - Repositories +shortTitle: View all repositories +type: how_to +--- + +## Viewing repositories + +Your repository dashboard is available at the top of any page. On the dashboard, you can see repositories by `My contributions`, `My repositories`, and `My forks`. + +1. At the top of any page, click **{% octicon "repo" aria-hidden="true" aria-label="repo" %}** to see your repositories. + +1. Optionally, choose a filter or use the search bar to filter for more specific results. Refine your search using filters like `visibility`, `language`, `organization`, and more. Sort by relevance to intelligently surface the repositories you're most active in. + +{% ifversion issues-saved-views %} + +## Tracking repositories with saved views + +To help you better monitor and find repositories across multiple organizations, you can create saved views on the repository dashboard. + +You can create up to 25 saved views. + +### Create a saved view + +1. At the top of any page, click **{% octicon "repo" aria-hidden="true" aria-label="repo" %}** to see your repositories. +1. On the left sidebar, under "Views", click **{% octicon "plus" aria-label="Create view" %}**. +1. Add a title, description, and custom icon for your view. +1. Under "Query", build your search query using the advanced filters. For help using filters, see [AUTOTITLE](/search-github/searching-on-github/searching-for-repositories). + > [!TIP] Use the `organization:` filter followed by `props.key:value` to find repositories by organization custom properties. +1. Click **Save view**. + +### Edit, duplicate, or delete a saved view + +1. At the top of any page, click **{% octicon "repo" aria-hidden="true" aria-label="repo" %}** to see your repositories. +1. On the left sidebar, under "Views", click the saved view you want to edit, duplicate or delete. +1. To the right of the name of the saved view, click **{% octicon "kebab-horizontal" aria-label="The horizontal kebab icon" %}**. +1. Click **{% octicon "pencil" aria-hidden="true" aria-label="pencil" %} Edit** to modify the view, **{% octicon "duplicate" aria-hidden="true" aria-label="duplicate" %} Duplicate** to create a copy of the view, or **{% octicon "trash" aria-hidden="true" aria-label="trash" %} Delete** to remove the view. + +{% endif %} diff --git a/data/reusables/code-quality/codeql-detection-intro.md b/data/reusables/code-quality/codeql-detection-intro.md new file mode 100644 index 000000000000..4e50b8a4ec19 --- /dev/null +++ b/data/reusables/code-quality/codeql-detection-intro.md @@ -0,0 +1,6 @@ +{% data variables.product.prodname_code_quality_short %} uses {% data variables.product.prodname_codeql %} analysis to detect code quality issues: + +* On your **default branch**, with results shown on the repository's "{% data variables.code-quality.all_findings %}" dashboard +* In **pull requests**, with findings shown as comments made by `{% data variables.code-quality.pr_commenter %}` + +{% data variables.copilot.copilot_autofix_short %} suggestions are provided for findings where possible. diff --git a/data/reusables/code-quality/codeql-query-tables/csharp.md b/data/reusables/code-quality/codeql-query-tables/csharp.md new file mode 100644 index 000000000000..7747570114e6 --- /dev/null +++ b/data/reusables/code-quality/codeql-query-tables/csharp.md @@ -0,0 +1,77 @@ +{% rowheaders %} + +| Query name | Category | Severity | +| --- | --- | --- | +| [Block code with a single Response.Write()](https://codeql.github.com/codeql-query-help/csharp/cs-asp-response-write/) | Maintainability | Recommendation | +| [Call to obsolete method](https://codeql.github.com/codeql-query-help/csharp/cs-call-to-obsolete-method/) | Maintainability | Warning | +| [Class has same name as super class](https://codeql.github.com/codeql-query-help/csharp/cs-class-name-matches-base-class/) | Maintainability | Recommendation | +| [Class implements ICloneable](https://codeql.github.com/codeql-query-help/csharp/cs-class-implements-icloneable/) | Maintainability | Recommendation | +| [Constant condition](https://codeql.github.com/codeql-query-help/csharp/cs-constant-condition/) | Maintainability | Warning | +| [Container contents are never accessed](https://codeql.github.com/codeql-query-help/csharp/cs-unused-collection/) | Maintainability | Error | +| [Field masks field in super class](https://codeql.github.com/codeql-query-help/csharp/cs-field-masks-base-field/) | Maintainability | Warning | +| [Futile conditional](https://codeql.github.com/codeql-query-help/csharp/cs-useless-if-statement/) | Maintainability | Warning | +| [Local scope variable shadows member](https://codeql.github.com/codeql-query-help/csharp/cs-local-shadows-member/) | Maintainability | Recommendation | +| [Missed 'readonly' opportunity](https://codeql.github.com/codeql-query-help/csharp/cs-missed-readonly-modifier/) | Maintainability | Recommendation | +| [Missed 'using' opportunity](https://codeql.github.com/codeql-query-help/csharp/cs-missed-using-statement/) | Maintainability | Recommendation | +| [Missed opportunity to use All](https://codeql.github.com/codeql-query-help/csharp/cs-linq-missed-all/) | Maintainability | Recommendation | +| [Missed opportunity to use Cast](https://codeql.github.com/codeql-query-help/csharp/cs-linq-missed-cast/) | Maintainability | Recommendation | +| [Missed opportunity to use OfType](https://codeql.github.com/codeql-query-help/csharp/cs-linq-missed-oftype/) | Maintainability | Recommendation | +| [Missed opportunity to use Select](https://codeql.github.com/codeql-query-help/csharp/cs-linq-missed-select/) | Maintainability | Recommendation | +| [Missed opportunity to use Where](https://codeql.github.com/codeql-query-help/csharp/cs-linq-missed-where/) | Maintainability | Recommendation | +| [Missed ternary opportunity](https://codeql.github.com/codeql-query-help/csharp/cs-missed-ternary-operator/) | Maintainability | Recommendation | +| [Nested 'if' statements can be combined](https://codeql.github.com/codeql-query-help/csharp/cs-nested-if-statements/) | Maintainability | Recommendation | +| [Redundant Select](https://codeql.github.com/codeql-query-help/csharp/cs-linq-useless-select/) | Maintainability | Warning | +| [Redundant ToString() call](https://codeql.github.com/codeql-query-help/csharp/cs-useless-tostring-call/) | Maintainability | Recommendation | +| [Static field written by instance method](https://codeql.github.com/codeql-query-help/csharp/cs-static-field-written-by-instance/) | Maintainability | Recommendation | +| [Unnecessarily complex Boolean expression](https://codeql.github.com/codeql-query-help/csharp/cs-simplifiable-boolean-expression/) | Maintainability | Recommendation | +| [Unused label](https://codeql.github.com/codeql-query-help/csharp/cs-unused-label/) | Maintainability | Warning | +| [Useless assignment to local variable](https://codeql.github.com/codeql-query-help/csharp/cs-useless-assignment-to-local/) | Maintainability | Warning | +| [Useless call to GetHashCode()](https://codeql.github.com/codeql-query-help/csharp/cs-useless-gethashcode-call/) | Maintainability | Recommendation | +| [A lock is held during a wait](https://codeql.github.com/codeql-query-help/csharp/cs-locked-wait/) | Reliability | Warning | +| [Call to GC.Collect()](https://codeql.github.com/codeql-query-help/csharp/cs-call-to-gc/) | Reliability | Warning | +| [Call to ReferenceEquals(...) on value type expressions](https://codeql.github.com/codeql-query-help/csharp/cs-reference-equality-on-valuetypes/) | Reliability | Error | +| [Call to System.IO.Path.Combine](https://codeql.github.com/codeql-query-help/csharp/cs-path-combine/) | Reliability | Recommendation | +| [Calls to unmanaged code](https://codeql.github.com/codeql-query-help/csharp/cs-call-to-unmanaged-code/) | Reliability | Recommendation | +| [Cast of 'this' to a type parameter](https://codeql.github.com/codeql-query-help/csharp/cs-cast-of-this-to-type-parameter/) | Reliability | Recommendation | +| [Character passed to StringBuilder constructor](https://codeql.github.com/codeql-query-help/csharp/cs-stringbuilder-initialized-with-character/) | Reliability | Error | +| [Comparison is constant](https://codeql.github.com/codeql-query-help/csharp/cs-constant-comparison/) | Reliability | Warning | +| [Comparison of identical values](https://codeql.github.com/codeql-query-help/csharp/cs-comparison-of-identical-expressions/) | Reliability | Warning | +| [Container contents are never initialized](https://codeql.github.com/codeql-query-help/csharp/cs-empty-collection/) | Reliability | Error | +| [Container size compared to zero](https://codeql.github.com/codeql-query-help/csharp/cs-test-for-negative-container-size/) | Reliability | Warning | +| [Dereferenced variable is always null](https://codeql.github.com/codeql-query-help/csharp/cs-dereferenced-value-is-always-null/) | Reliability | Error | +| [Dereferenced variable may be null](https://codeql.github.com/codeql-query-help/csharp/cs-dereferenced-value-may-be-null/) | Reliability | Warning | +| [Dubious downcast of 'this'](https://codeql.github.com/codeql-query-help/csharp/cs-downcast-of-this/) | Reliability | Warning | +| [Dubious type test of 'this'](https://codeql.github.com/codeql-query-help/csharp/cs-type-test-of-this/) | Reliability | Warning | +| [Empty branch of conditional, or empty loop body](https://codeql.github.com/codeql-query-help/csharp/cs-empty-block/) | Reliability | Warning | +| [Empty lock statement](https://codeql.github.com/codeql-query-help/csharp/cs-empty-lock-statement/) | Reliability | Warning | +| [Equality check on floating point values](https://codeql.github.com/codeql-query-help/csharp/cs-equality-on-floats/) | Reliability | Warning | +| [Equals on collections](https://codeql.github.com/codeql-query-help/csharp/cs-equals-on-arrays/) | Reliability | Recommendation | +| [Equals on incomparable types](https://codeql.github.com/codeql-query-help/csharp/cs-equals-on-unrelated-types/) | Reliability | Error | +| [Exposing internal representation](https://codeql.github.com/codeql-query-help/csharp/cs-expose-implementation/) | Reliability | Recommendation | +| [Futile synchronization on field](https://codeql.github.com/codeql-query-help/csharp/cs-unsafe-sync-on-field/) | Reliability | Error | +| [Generic catch clause](https://codeql.github.com/codeql-query-help/csharp/cs-catch-of-all-exceptions/) | Reliability | Recommendation | +| [Hashed value without GetHashCode definition](https://codeql.github.com/codeql-query-help/csharp/cs-gethashcode-is-not-defined/) | Reliability | Warning | +| [Impossible array cast](https://codeql.github.com/codeql-query-help/csharp/cs-impossible-array-cast/) | Reliability | Error | +| [Inconsistent lock sequence](https://codeql.github.com/codeql-query-help/csharp/cs-inconsistent-lock-sequence/) | Reliability | Error | +| [Inefficient use of ContainsKey](https://codeql.github.com/codeql-query-help/csharp/cs-inefficient-containskey/) | Reliability | Recommendation | +| [Invalid string formatting](https://codeql.github.com/codeql-query-help/csharp/cs-invalid-string-formatting/) | Reliability | Error | +| [Locking the 'this' object in a lock statement](https://codeql.github.com/codeql-query-help/csharp/cs-lock-this/) | Reliability | Warning | +| [Missing Dispose call on local IDisposable](https://codeql.github.com/codeql-query-help/csharp/cs-local-not-disposed/) | Reliability | Warning | +| [Nested loops with same variable](https://codeql.github.com/codeql-query-help/csharp/cs-nested-loops-with-same-variable/) | Reliability | Warning | +| [Null argument to Equals(object)](https://codeql.github.com/codeql-query-help/csharp/cs-null-argument-to-equals/) | Reliability | Warning | +| [Off-by-one comparison against container length](https://codeql.github.com/codeql-query-help/csharp/cs-index-out-of-bounds/) | Reliability | Error | +| [Poor error handling: catch of NullReferenceException](https://codeql.github.com/codeql-query-help/csharp/cs-catch-nullreferenceexception/) | Reliability | Warning | +| [Poor error handling: empty catch block](https://codeql.github.com/codeql-query-help/csharp/cs-empty-catch-block/) | Reliability | Recommendation | +| [Possible loss of precision](https://codeql.github.com/codeql-query-help/csharp/cs-loss-of-precision/) | Reliability | Error | +| [Potentially dangerous use of non-short-circuit logic](https://codeql.github.com/codeql-query-help/csharp/cs-non-short-circuit/) | Reliability | Error | +| [Property value is not used when setting a property](https://codeql.github.com/codeql-query-help/csharp/cs-unused-property-value/) | Reliability | Warning | +| [Recursive call to Equals(object)](https://codeql.github.com/codeql-query-help/csharp/cs-recursive-equals-call/) | Reliability | Error | +| [Rethrowing exception variable](https://codeql.github.com/codeql-query-help/csharp/cs-rethrown-exception-variable/) | Reliability | Warning | +| [Self-assignment](https://codeql.github.com/codeql-query-help/csharp/cs-self-assignment/) | Reliability | Error | +| [String concatenation in loop](https://codeql.github.com/codeql-query-help/csharp/cs-string-concatenation-in-loop/) | Reliability | Recommendation | +| [StringBuilder creation in loop](https://codeql.github.com/codeql-query-help/csharp/cs-stringbuilder-creation-in-loop/) | Reliability | Recommendation | +| [Unchecked cast in Equals method](https://codeql.github.com/codeql-query-help/csharp/cs-unchecked-cast-in-equals/) | Reliability | Warning | +| [Unmanaged code](https://codeql.github.com/codeql-query-help/csharp/cs-unmanaged-code/) | Reliability | Recommendation | +| [Use of default ToString()](https://codeql.github.com/codeql-query-help/csharp/cs-call-to-object-tostring/) | Reliability | Warning | + +{% endrowheaders %} diff --git a/data/reusables/code-quality/codeql-query-tables/go.md b/data/reusables/code-quality/codeql-query-tables/go.md new file mode 100644 index 000000000000..6d6f07987b9a --- /dev/null +++ b/data/reusables/code-quality/codeql-query-tables/go.md @@ -0,0 +1,28 @@ +{% rowheaders %} + +| Query name | Category | Severity | +| --- | --- | --- | +| [Useless assignment to field](https://codeql.github.com/codeql-query-help/go/go-useless-assignment-to-field/) | Maintainability | Warning | +| [Useless assignment to local variable](https://codeql.github.com/codeql-query-help/go/go-useless-assignment-to-local/) | Maintainability | Warning | +| [Bitwise exclusive-or used like exponentiation](https://codeql.github.com/codeql-query-help/go/go-mistyped-exponentiation/) | Reliability | Warning | +| [Comparison of identical values](https://codeql.github.com/codeql-query-help/go/go-comparison-of-identical-expressions/) | Reliability | Warning | +| [Constant length comparison](https://codeql.github.com/codeql-query-help/go/go-constant-length-comparison/) | Reliability | Warning | +| [Duplicate 'if' branches](https://codeql.github.com/codeql-query-help/go/go-duplicate-branches/) | Reliability | Warning | +| [Duplicate 'if' condition](https://codeql.github.com/codeql-query-help/go/go-duplicate-condition/) | Reliability | Error | +| [Duplicate switch case](https://codeql.github.com/codeql-query-help/go/go-duplicate-switch-case/) | Reliability | Error | +| [Expression has no effect](https://codeql.github.com/codeql-query-help/go/go-useless-expression/) | Reliability | Warning | +| [Identical operands](https://codeql.github.com/codeql-query-help/go/go-redundant-operation/) | Reliability | Warning | +| [Impossible interface nil check](https://codeql.github.com/codeql-query-help/go/go-impossible-interface-nil-check/) | Reliability | Warning | +| [Inconsistent direction of for loop](https://codeql.github.com/codeql-query-help/go/go-inconsistent-loop-direction/) | Reliability | Error | +| [Missing error check](https://codeql.github.com/codeql-query-help/go/go-missing-error-check/) | Reliability | Warning | +| [Off-by-one comparison against length](https://codeql.github.com/codeql-query-help/go/go-index-out-of-bounds/) | Reliability | Error | +| [Redundant call to recover](https://codeql.github.com/codeql-query-help/go/go-redundant-recover/) | Reliability | Warning | +| [Redundant check for negative value](https://codeql.github.com/codeql-query-help/go/go-negative-length-check/) | Reliability | Warning | +| [Self assignment](https://codeql.github.com/codeql-query-help/go/go-redundant-assignment/) | Reliability | Warning | +| [Shift out of range](https://codeql.github.com/codeql-query-help/go/go-shift-out-of-range/) | Reliability | Warning | +| [Unreachable statement](https://codeql.github.com/codeql-query-help/go/go-unreachable-statement/) | Reliability | Warning | +| [Whitespace contradicts operator precedence](https://codeql.github.com/codeql-query-help/go/go-whitespace-contradicts-precedence/) | Reliability | Warning | +| [Wrapped error is always nil](https://codeql.github.com/codeql-query-help/go/go-unexpected-nil-value/) | Reliability | Warning | +| [Writable file handle closed without error handling](https://codeql.github.com/codeql-query-help/go/go-unhandled-writable-file-close/) | Reliability | Warning | + +{% endrowheaders %} diff --git a/data/reusables/code-quality/codeql-query-tables/java.md b/data/reusables/code-quality/codeql-query-tables/java.md new file mode 100644 index 000000000000..c7ae68164570 --- /dev/null +++ b/data/reusables/code-quality/codeql-query-tables/java.md @@ -0,0 +1,95 @@ +{% rowheaders %} + +| Query name | Category | Severity | +| --- | --- | --- | +| [Access to unsupported JDK-internal API](https://codeql.github.com/codeql-query-help/java/java-jdk-internal-api-access/) | Maintainability | Recommendation | +| [Boxed variable is never null](https://codeql.github.com/codeql-query-help/java/java-non-null-boxed-variable/) | Maintainability | Warning | +| [Cast from abstract to concrete collection](https://codeql.github.com/codeql-query-help/java/java-abstract-to-concrete-cast/) | Maintainability | Warning | +| [Class has same name as super class](https://codeql.github.com/codeql-query-help/java/java-class-name-matches-super-class/) | Maintainability | Recommendation | +| [Confusing method names because of capitalization](https://codeql.github.com/codeql-query-help/java/java-confusing-method-name/) | Maintainability | Recommendation | +| [Confusing overloading of methods](https://codeql.github.com/codeql-query-help/java/java-confusing-method-signature/) | Maintainability | Recommendation | +| [Constant interface anti-pattern](https://codeql.github.com/codeql-query-help/java/java-constants-only-interface/) | Maintainability | Recommendation | +| [Container contents are never accessed](https://codeql.github.com/codeql-query-help/java/java-unused-container/) | Maintainability | Error | +| [Inefficient empty string test](https://codeql.github.com/codeql-query-help/java/java-inefficient-empty-string-test/) | Maintainability | Recommendation | +| [Inefficient String constructor](https://codeql.github.com/codeql-query-help/java/java-inefficient-string-constructor/) | Maintainability | Recommendation | +| [Inner class could be static](https://codeql.github.com/codeql-query-help/java/java-non-static-nested-class/) | Maintainability | Recommendation | +| [Interface cannot be implemented](https://codeql.github.com/codeql-query-help/java/java-unimplementable-interface/) | Maintainability | Warning | +| [Javadoc has impossible 'throws' tag](https://codeql.github.com/codeql-query-help/java/java-inconsistent-javadoc-throws/) | Maintainability | Recommendation | +| [Misleading indentation](https://codeql.github.com/codeql-query-help/java/java-misleading-indentation/) | Maintainability | Warning | +| [Missing Override annotation](https://codeql.github.com/codeql-query-help/java/java-missing-override-annotation/) | Maintainability | Recommendation | +| [Missing space in string literal](https://codeql.github.com/codeql-query-help/java/java-missing-space-in-concatenation/) | Maintainability | Recommendation | +| [Mocking all public methods of a class may indicate the unit test is testing too much](https://codeql.github.com/codeql-query-help/java/java-excessive-public-method-mocking/) | Maintainability | Recommendation | +| [Non-case label in switch statement](https://codeql.github.com/codeql-query-help/java/java-label-in-switch/) | Maintainability | Recommendation | +| [Non-explicit control and whitespace characters](https://codeql.github.com/codeql-query-help/java/java-non-explicit-control-and-whitespace-chars-in-literals/) | Maintainability | Warning | +| [Possible confusion of local and field](https://codeql.github.com/codeql-query-help/java/java-local-shadows-field/) | Maintainability | Recommendation | +| [Spurious Javadoc @param tags](https://codeql.github.com/codeql-query-help/java/java-unknown-javadoc-parameter/) | Maintainability | Recommendation | +| [Subtle call to inherited method](https://codeql.github.com/codeql-query-help/java/java-subtle-inherited-call/) | Maintainability | Warning | +| [Underscore used as identifier](https://codeql.github.com/codeql-query-help/java/java-underscore-identifier/) | Maintainability | Recommendation | +| [Unread local variable](https://codeql.github.com/codeql-query-help/java/java-local-variable-is-never-read/) | Maintainability | Recommendation | +| [Unused classes and interfaces](https://codeql.github.com/codeql-query-help/java/java-unused-reference-type/) | Maintainability | Recommendation | +| [Unused format argument](https://codeql.github.com/codeql-query-help/java/java-unused-format-argument/) | Maintainability | Warning | +| [Unused label](https://codeql.github.com/codeql-query-help/java/java-unused-label/) | Maintainability | Recommendation | +| [Use of VisibleForTesting in production code](https://codeql.github.com/codeql-query-help/java/java-visible-for-testing-abuse/) | Maintainability | Warning | +| [Useless null check](https://codeql.github.com/codeql-query-help/java/java-useless-null-check/) | Maintainability | Warning | +| [Useless parameter](https://codeql.github.com/codeql-query-help/java/java-unused-parameter/) | Maintainability | Recommendation | +| [Useless toString on String](https://codeql.github.com/codeql-query-help/java/java-useless-tostring-call/) | Maintainability | Recommendation | +| [Useless type test](https://codeql.github.com/codeql-query-help/java/java-useless-type-test/) | Maintainability | Warning | +| [Array index out of bounds](https://codeql.github.com/codeql-query-help/java/java-index-out-of-bounds/) | Reliability | Error | +| [Character passed to StringBuffer or StringBuilder constructor](https://codeql.github.com/codeql-query-help/java/java-string-buffer-char-init/) | Reliability | Error | +| [Comparison of identical values](https://codeql.github.com/codeql-query-help/java/java-comparison-of-identical-expressions/) | Reliability | Error | +| [Constant loop condition](https://codeql.github.com/codeql-query-help/java/java-constant-loop-condition/) | Reliability | Warning | +| [Container contents are never initialized](https://codeql.github.com/codeql-query-help/java/java-empty-container/) | Reliability | Error | +| [Container size compared to zero](https://codeql.github.com/codeql-query-help/java/java-test-for-negative-container-size/) | Reliability | Warning | +| [Continue statement that does not continue](https://codeql.github.com/codeql-query-help/java/java-continue-in-false-loop/) | Reliability | Warning | +| [Contradictory type checks](https://codeql.github.com/codeql-query-help/java/java-contradictory-type-checks/) | Reliability | Error | +| [Dereferenced expression may be null](https://codeql.github.com/codeql-query-help/java/java-dereferenced-expr-may-be-null/) | Reliability | Warning | +| [Dereferenced variable is always null](https://codeql.github.com/codeql-query-help/java/java-dereferenced-value-is-always-null/) | Reliability | Error | +| [Dereferenced variable may be null](https://codeql.github.com/codeql-query-help/java/java-dereferenced-value-may-be-null/) | Reliability | Warning | +| [Direct call to a run() method](https://codeql.github.com/codeql-query-help/java/java-call-to-thread-run/) | Reliability | Recommendation | +| [Do not call `finalize()`](https://codeql.github.com/codeql-query-help/java/java-do-not-call-finalize/) | Reliability | Error | +| [Double-checked locking is not thread-safe](https://codeql.github.com/codeql-query-help/java/java-unsafe-double-checked-locking/) | Reliability | Error | +| [Equals method does not inspect argument type](https://codeql.github.com/codeql-query-help/java/java-unchecked-cast-in-equals/) | Reliability | Error | +| [Equals on incomparable types](https://codeql.github.com/codeql-query-help/java/java-equals-on-unrelated-types/) | Reliability | Error | +| [Equals or hashCode on arrays](https://codeql.github.com/codeql-query-help/java/java-equals-on-arrays/) | Reliability | Error | +| [Escaping](https://codeql.github.com/codeql-query-help/java/java-escaping/) | Reliability | Warning | +| [Exposing internal representation](https://codeql.github.com/codeql-query-help/java/java-internal-representation-exposure/) | Reliability | Recommendation | +| [Expression always evaluates to the same value](https://codeql.github.com/codeql-query-help/java/java-evaluation-to-constant/) | Reliability | Warning | +| [Hashed value without hashCode definition](https://codeql.github.com/codeql-query-help/java/java-hashing-without-hashcode/) | Reliability | Error | +| [Ignored error status of call](https://codeql.github.com/codeql-query-help/java/java-ignored-error-status-of-call/) | Reliability | Recommendation | +| [Ignored serialization member of record class](https://codeql.github.com/codeql-query-help/java/java-ignored-serialization-member-of-record-class/) | Reliability | Warning | +| [Implicit conversion from array to string](https://codeql.github.com/codeql-query-help/java/java-print-array/) | Reliability | Recommendation | +| [Inconsistent equals and hashCode](https://codeql.github.com/codeql-query-help/java/java-inconsistent-equals-and-hashcode/) | Reliability | Error | +| [Inconsistent synchronization of getter and setter](https://codeql.github.com/codeql-query-help/java/java-unsynchronized-getter/) | Reliability | Error | +| [Inefficient output stream](https://codeql.github.com/codeql-query-help/java/java-inefficient-output-stream/) | Reliability | Warning | +| [Inefficient primitive constructor](https://codeql.github.com/codeql-query-help/java/java-inefficient-boxed-constructor/) | Reliability | Recommendation | +| [Inefficient use of key set iterator](https://codeql.github.com/codeql-query-help/java/java-inefficient-key-set-iterator/) | Reliability | Recommendation | +| [Iterable wrapping an iterator](https://codeql.github.com/codeql-query-help/java/java-iterable-wraps-iterator/) | Reliability | Warning | +| [Iterator implementing Iterable](https://codeql.github.com/codeql-query-help/java/java-iterator-implements-iterable/) | Reliability | Warning | +| [Left shift by more than the type width](https://codeql.github.com/codeql-query-help/java/java-lshift-larger-than-type-width/) | Reliability | Warning | +| [Missing `@Nested` annotation on JUnit 5 inner test class](https://codeql.github.com/codeql-query-help/java/java-junit5-missing-nested-annotation/) | Reliability | Warning | +| [Missing catch of NumberFormatException](https://codeql.github.com/codeql-query-help/java/java-uncaught-number-format-exception/) | Reliability | Recommendation | +| [Missing format argument](https://codeql.github.com/codeql-query-help/java/java-missing-format-argument/) | Reliability | Error | +| [Non-final method invocation in constructor](https://codeql.github.com/codeql-query-help/java/java-non-final-call-in-constructor/) | Reliability | Error | +| [Non-synchronized override of synchronized method](https://codeql.github.com/codeql-query-help/java/java-non-sync-override/) | Reliability | Warning | +| [Not thread-safe](https://codeql.github.com/codeql-query-help/java/java-not-threadsafe/) | Reliability | Warning | +| [Potential database resource leak](https://codeql.github.com/codeql-query-help/java/java-database-resource-leak/) | Reliability | Warning | +| [Potential input resource leak](https://codeql.github.com/codeql-query-help/java/java-input-resource-leak/) | Reliability | Warning | +| [Potential output resource leak](https://codeql.github.com/codeql-query-help/java/java-output-resource-leak/) | Reliability | Warning | +| [Race condition in double-checked locking object initialization](https://codeql.github.com/codeql-query-help/java/java-unsafe-double-checked-locking-init-order/) | Reliability | Warning | +| [Reference equality test of boxed types](https://codeql.github.com/codeql-query-help/java/java-reference-equality-of-boxed-types/) | Reliability | Error | +| [Result of multiplication cast to wider type](https://codeql.github.com/codeql-query-help/java/java-integer-multiplication-cast-to-long/) | Reliability | Warning | +| [Safe publication](https://codeql.github.com/codeql-query-help/java/java-safe-publication/) | Reliability | Warning | +| [Self assignment](https://codeql.github.com/codeql-query-help/java/java-redundant-assignment/) | Reliability | Error | +| [Suspicious date format](https://codeql.github.com/codeql-query-help/java/java-suspicious-date-format/) | Reliability | Warning | +| [Synchronization on boxed types or strings](https://codeql.github.com/codeql-query-help/java/java-sync-on-boxed-types/) | Reliability | Error | +| [Type mismatch on container access](https://codeql.github.com/codeql-query-help/java/java-type-mismatch-access/) | Reliability | Error | +| [Type mismatch on container modification](https://codeql.github.com/codeql-query-help/java/java-type-mismatch-modification/) | Reliability | Error | +| [Unreachable catch clause](https://codeql.github.com/codeql-query-help/java/java-unreachable-catch-clause/) | Reliability | Warning | +| [Use of `String#replaceAll` with a first argument which is not a regular expression](https://codeql.github.com/codeql-query-help/java/java-string-replace-all-with-non-regex/) | Reliability | Recommendation | +| [Use of default toString()](https://codeql.github.com/codeql-query-help/java/java-call-to-object-tostring/) | Reliability | Recommendation | +| [Useless comparison test](https://codeql.github.com/codeql-query-help/java/java-constant-comparison/) | Reliability | Warning | +| [Whitespace contradicts operator precedence](https://codeql.github.com/codeql-query-help/java/java-whitespace-contradicts-precedence/) | Reliability | Warning | +| [Wrong NaN comparison](https://codeql.github.com/codeql-query-help/java/java-comparison-with-nan/) | Reliability | Error | +| [Zero threads set for `java.util.concurrent.ScheduledThreadPoolExecutor`](https://codeql.github.com/codeql-query-help/java/java-java-util-concurrent-scheduledthreadpoolexecutor/) | Reliability | Recommendation | + +{% endrowheaders %} diff --git a/data/reusables/code-quality/codeql-query-tables/javascript.md b/data/reusables/code-quality/codeql-query-tables/javascript.md new file mode 100644 index 000000000000..05e09d5629ff --- /dev/null +++ b/data/reusables/code-quality/codeql-query-tables/javascript.md @@ -0,0 +1,104 @@ +{% rowheaders %} + +| Query name | Category | Severity | +| --- | --- | --- | +| [Call to eval-like DOM function](https://codeql.github.com/codeql-query-help/javascript/js-eval-like-call/) | Maintainability | Recommendation | +| [Duplicate dependency](https://codeql.github.com/codeql-query-help/javascript/js-angular-duplicate-dependency/) | Maintainability | Warning | +| [Duplicate HTML element attributes](https://codeql.github.com/codeql-query-help/javascript/js-duplicate-html-attribute/) | Maintainability | Warning | +| [Duplicate property](https://codeql.github.com/codeql-query-help/javascript/js-duplicate-property/) | Maintainability | Warning | +| [Duplicate variable declaration](https://codeql.github.com/codeql-query-help/javascript/js-duplicate-variable-declaration/) | Maintainability | Recommendation | +| [Expression has no effect](https://codeql.github.com/codeql-query-help/javascript/js-useless-expression/) | Maintainability | Warning | +| [Misleading indentation after control statement](https://codeql.github.com/codeql-query-help/javascript/js-misleading-indentation-after-control-statement/) | Maintainability | Warning | +| [Misleading indentation of dangling 'else'](https://codeql.github.com/codeql-query-help/javascript/js-misleading-indentation-of-dangling-else/) | Maintainability | Warning | +| [Missing space in string concatenation](https://codeql.github.com/codeql-query-help/javascript/js-missing-space-in-concatenation/) | Maintainability | Warning | +| [Repeated dependency injection](https://codeql.github.com/codeql-query-help/javascript/js-angular-repeated-dependency-injection/) | Maintainability | Warning | +| [Semicolon insertion](https://codeql.github.com/codeql-query-help/javascript/js-automatic-semicolon-insertion/) | Maintainability | Recommendation | +| [Unclear precedence of nested operators](https://codeql.github.com/codeql-query-help/javascript/js-unclear-operator-precedence/) | Maintainability | Recommendation | +| [Unneeded defensive code](https://codeql.github.com/codeql-query-help/javascript/js-unneeded-defensive-code/) | Maintainability | Recommendation | +| [Unused variable, import, function or class](https://codeql.github.com/codeql-query-help/javascript/js-unused-local-variable/) | Maintainability | Recommendation | +| [Use of for-in comprehension blocks](https://codeql.github.com/codeql-query-help/javascript/js-for-in-comprehension/) | Maintainability | Error | +| [Use of platform-specific language features](https://codeql.github.com/codeql-query-help/javascript/js-non-standard-language-feature/) | Maintainability | Warning | +| [Useless assignment to local variable](https://codeql.github.com/codeql-query-help/javascript/js-useless-assignment-to-local/) | Maintainability | Warning | +| [Useless assignment to property](https://codeql.github.com/codeql-query-help/javascript/js-useless-assignment-to-property/) | Maintainability | Warning | +| [Useless return in setter](https://codeql.github.com/codeql-query-help/javascript/js-setter-return/) | Maintainability | Warning | +| [Variable not declared before use](https://codeql.github.com/codeql-query-help/javascript/js-use-before-declaration/) | Maintainability | Warning | +| [With statement](https://codeql.github.com/codeql-query-help/javascript/js-with-statement/) | Maintainability | Warning | +| [Access to let-bound variable in temporal dead zone](https://codeql.github.com/codeql-query-help/javascript/js-variable-use-in-temporal-dead-zone/) | Reliability | Error | +| [Arguments redefined](https://codeql.github.com/codeql-query-help/javascript/js-arguments-redefinition/) | Reliability | Recommendation | +| [Arrow method on Vue instance](https://codeql.github.com/codeql-query-help/javascript/js-vue-arrow-method-on-vue-instance/) | Reliability | Warning | +| [Assignment to constant](https://codeql.github.com/codeql-query-help/javascript/js-assignment-to-constant/) | Reliability | Error | +| [Assignment to exports variable](https://codeql.github.com/codeql-query-help/javascript/js-node-assignment-to-exports-variable/) | Reliability | Warning | +| [Assignment to property of primitive value](https://codeql.github.com/codeql-query-help/javascript/js-property-assignment-on-primitive/) | Reliability | Error | +| [Back reference into negative lookahead assertion](https://codeql.github.com/codeql-query-help/javascript/js-regex-back-reference-to-negative-lookahead/) | Reliability | Error | +| [Back reference precedes capture group](https://codeql.github.com/codeql-query-help/javascript/js-regex-back-reference-before-group/) | Reliability | Error | +| [Comparison between inconvertible types](https://codeql.github.com/codeql-query-help/javascript/js-comparison-between-incompatible-types/) | Reliability | Warning | +| [Comparison with NaN](https://codeql.github.com/codeql-query-help/javascript/js-comparison-with-nan/) | Reliability | Error | +| [Conditional comments](https://codeql.github.com/codeql-query-help/javascript/js-conditional-comment/) | Reliability | Warning | +| [Conflicting function declarations](https://codeql.github.com/codeql-query-help/javascript/js-function-declaration-conflict/) | Reliability | Error | +| [Conflicting variable initialization](https://codeql.github.com/codeql-query-help/javascript/js-variable-initialization-conflict/) | Reliability | Error | +| [Default parameter references nested function](https://codeql.github.com/codeql-query-help/javascript/js-nested-function-reference-in-default-parameter/) | Reliability | Error | +| [Deleting non-property](https://codeql.github.com/codeql-query-help/javascript/js-deletion-of-non-property/) | Reliability | Warning | +| [Dependency mismatch](https://codeql.github.com/codeql-query-help/javascript/js-angular-dependency-injection-mismatch/) | Reliability | Warning | +| [Direct state mutation](https://codeql.github.com/codeql-query-help/javascript/js-react-direct-state-mutation/) | Reliability | Warning | +| [Duplicate 'if' condition](https://codeql.github.com/codeql-query-help/javascript/js-duplicate-condition/) | Reliability | Warning | +| [Duplicate character in character class](https://codeql.github.com/codeql-query-help/javascript/js-regex-duplicate-in-character-class/) | Reliability | Warning | +| [Duplicate parameter names](https://codeql.github.com/codeql-query-help/javascript/js-duplicate-parameter-name/) | Reliability | Error | +| [Duplicate switch case](https://codeql.github.com/codeql-query-help/javascript/js-duplicate-switch-case/) | Reliability | Warning | +| [Empty character class](https://codeql.github.com/codeql-query-help/javascript/js-regex-empty-character-class/) | Reliability | Warning | +| [Identical operands](https://codeql.github.com/codeql-query-help/javascript/js-redundant-operation/) | Reliability | Warning | +| [Ignoring result from pure array method](https://codeql.github.com/codeql-query-help/javascript/js-ignore-array-result/) | Reliability | Warning | +| [Illegal invocation](https://codeql.github.com/codeql-query-help/javascript/js-illegal-invocation/) | Reliability | Error | +| [Implicit operand conversion](https://codeql.github.com/codeql-query-help/javascript/js-implicit-operand-conversion/) | Reliability | Warning | +| [Incompatible dependency injection](https://codeql.github.com/codeql-query-help/javascript/js-angular-incompatible-service/) | Reliability | Error | +| [Inconsistent direction of for loop](https://codeql.github.com/codeql-query-help/javascript/js-inconsistent-loop-direction/) | Reliability | Error | +| [Inconsistent use of 'new'](https://codeql.github.com/codeql-query-help/javascript/js-inconsistent-use-of-new/) | Reliability | Warning | +| [Ineffective parameter type](https://codeql.github.com/codeql-query-help/javascript/js-ineffective-parameter-type/) | Reliability | Warning | +| [Invalid prototype value](https://codeql.github.com/codeql-query-help/javascript/js-invalid-prototype-value/) | Reliability | Error | +| [Invocation of non-function](https://codeql.github.com/codeql-query-help/javascript/js-call-to-non-callable/) | Reliability | Error | +| [Loop iteration skipped due to shifting](https://codeql.github.com/codeql-query-help/javascript/js-loop-iteration-skipped-due-to-shifting/) | Reliability | Warning | +| [Malformed id attribute](https://codeql.github.com/codeql-query-help/javascript/js-malformed-html-id/) | Reliability | Warning | +| [Missing '.length' in comparison](https://codeql.github.com/codeql-query-help/javascript/js-missing-dot-length-in-comparison/) | Reliability | Warning | +| [Missing 'this' qualifier](https://codeql.github.com/codeql-query-help/javascript/js-missing-this-qualifier/) | Reliability | Error | +| [Missing await](https://codeql.github.com/codeql-query-help/javascript/js-missing-await/) | Reliability | Warning | +| [Missing explicit dependency injection](https://codeql.github.com/codeql-query-help/javascript/js-angular-missing-explicit-injection/) | Reliability | Warning | +| [Missing exports qualifier](https://codeql.github.com/codeql-query-help/javascript/js-node-missing-exports-qualifier/) | Reliability | Error | +| [Missing variable declaration](https://codeql.github.com/codeql-query-help/javascript/js-missing-variable-declaration/) | Reliability | Warning | +| [Misspelled variable name](https://codeql.github.com/codeql-query-help/javascript/js-misspelled-variable-name/) | Reliability | Warning | +| [Non-case label in switch statement](https://codeql.github.com/codeql-query-help/javascript/js-label-in-switch/) | Reliability | Warning | +| [Non-linear pattern](https://codeql.github.com/codeql-query-help/javascript/js-non-linear-pattern/) | Reliability | Error | +| [Off-by-one comparison against length](https://codeql.github.com/codeql-query-help/javascript/js-index-out-of-bounds/) | Reliability | Warning | +| [Overwritten property](https://codeql.github.com/codeql-query-help/javascript/js-overwritten-property/) | Reliability | Error | +| [Potentially inconsistent state update](https://codeql.github.com/codeql-query-help/javascript/js-react-inconsistent-state-update/) | Reliability | Warning | +| [Property access on null or undefined](https://codeql.github.com/codeql-query-help/javascript/js-property-access-on-non-object/) | Reliability | Error | +| [Regular expression always matches](https://codeql.github.com/codeql-query-help/javascript/js-regex-always-matches/) | Reliability | Warning | +| [Return statement assigns local variable](https://codeql.github.com/codeql-query-help/javascript/js-useless-assignment-in-return/) | Reliability | Warning | +| [Self assignment](https://codeql.github.com/codeql-query-help/javascript/js-redundant-assignment/) | Reliability | Warning | +| [Shift out of range](https://codeql.github.com/codeql-query-help/javascript/js-shift-out-of-range/) | Reliability | Error | +| [String instead of regular expression](https://codeql.github.com/codeql-query-help/javascript/js-string-instead-of-regex/) | Reliability | Warning | +| [Superfluous trailing arguments](https://codeql.github.com/codeql-query-help/javascript/js-superfluous-trailing-arguments/) | Reliability | Warning | +| [Suspicious method name declaration](https://codeql.github.com/codeql-query-help/javascript/js-suspicious-method-name-declaration/) | Reliability | Warning | +| [Template syntax in string literal](https://codeql.github.com/codeql-query-help/javascript/js-template-syntax-in-string-literal/) | Reliability | Warning | +| [Unbound back reference](https://codeql.github.com/codeql-query-help/javascript/js-regex-unbound-back-reference/) | Reliability | Warning | +| [Unbound event handler receiver](https://codeql.github.com/codeql-query-help/javascript/js-unbound-event-handler-receiver/) | Reliability | Error | +| [Unhandled error in stream pipeline](https://codeql.github.com/codeql-query-help/javascript/js-unhandled-error-in-stream-pipeline/) | Reliability | Warning | +| [Unknown directive](https://codeql.github.com/codeql-query-help/javascript/js-unknown-directive/) | Reliability | Warning | +| [Unmatchable caret in regular expression](https://codeql.github.com/codeql-query-help/javascript/js-regex-unmatchable-caret/) | Reliability | Error | +| [Unmatchable dollar in regular expression](https://codeql.github.com/codeql-query-help/javascript/js-regex-unmatchable-dollar/) | Reliability | Error | +| [Unreachable method overloads](https://codeql.github.com/codeql-query-help/javascript/js-unreachable-method-overloads/) | Reliability | Warning | +| [Unreachable statement](https://codeql.github.com/codeql-query-help/javascript/js-unreachable-statement/) | Reliability | Warning | +| [Unsupported state update in lifecycle method](https://codeql.github.com/codeql-query-help/javascript/js-react-unsupported-state-update-in-lifecycle-method/) | Reliability | Warning | +| [Unused index variable](https://codeql.github.com/codeql-query-help/javascript/js-unused-index-variable/) | Reliability | Warning | +| [Unused loop iteration variable](https://codeql.github.com/codeql-query-help/javascript/js-unused-loop-variable/) | Reliability | Error | +| [Unused or undefined state property](https://codeql.github.com/codeql-query-help/javascript/js-react-unused-or-undefined-state-property/) | Reliability | Warning | +| [Use of AngularJS markup in URL-valued attribute](https://codeql.github.com/codeql-query-help/javascript/js-angular-expression-in-url-attribute/) | Reliability | Warning | +| [Use of call stack introspection in strict mode](https://codeql.github.com/codeql-query-help/javascript/js-strict-mode-call-stack-introspection/) | Reliability | Error | +| [Use of incompletely initialized object](https://codeql.github.com/codeql-query-help/javascript/js-incomplete-object-initialization/) | Reliability | Error | +| [Use of returnless function](https://codeql.github.com/codeql-query-help/javascript/js-use-of-returnless-function/) | Reliability | Warning | +| [Useless comparison test](https://codeql.github.com/codeql-query-help/javascript/js-useless-comparison-test/) | Reliability | Warning | +| [Useless conditional](https://codeql.github.com/codeql-query-help/javascript/js-trivial-conditional/) | Reliability | Warning | +| [Useless type test](https://codeql.github.com/codeql-query-help/javascript/js-useless-type-test/) | Reliability | Error | +| [Whitespace contradicts operator precedence](https://codeql.github.com/codeql-query-help/javascript/js-whitespace-contradicts-precedence/) | Reliability | Warning | +| [Wrong use of 'this' for static method](https://codeql.github.com/codeql-query-help/javascript/js-mixed-static-instance-this-access/) | Reliability | Error | +| [Yield in non-generator function](https://codeql.github.com/codeql-query-help/javascript/js-yield-outside-generator/) | Reliability | Error | + +{% endrowheaders %} diff --git a/data/reusables/code-quality/codeql-query-tables/python.md b/data/reusables/code-quality/codeql-query-tables/python.md new file mode 100644 index 000000000000..3360acd49b7c --- /dev/null +++ b/data/reusables/code-quality/codeql-query-tables/python.md @@ -0,0 +1,107 @@ +{% rowheaders %} + +| Query name | Category | Severity | +| --- | --- | --- | +| ['import *' may pollute namespace](https://codeql.github.com/codeql-query-help/python/py-polluting-import/) | Maintainability | Recommendation | +| [Backspace escape in regular expression](https://codeql.github.com/codeql-query-help/python/py-regex-backspace-escape/) | Maintainability | Recommendation | +| [Commented-out code](https://codeql.github.com/codeql-query-help/python/py-commented-out-code/) | Maintainability | Recommendation | +| [Comparison of constants](https://codeql.github.com/codeql-query-help/python/py-comparison-of-constants/) | Maintainability | Warning | +| [Comparison of identical values](https://codeql.github.com/codeql-query-help/python/py-comparison-of-identical-expressions/) | Maintainability | Warning | +| [Constant in conditional expression or statement](https://codeql.github.com/codeql-query-help/python/py-constant-conditional-expression/) | Maintainability | Warning | +| [Duplicate key in dict literal](https://codeql.github.com/codeql-query-help/python/py-duplicate-key-dict-literal/) | Maintainability | Warning | +| [Duplication in regular expression character class](https://codeql.github.com/codeql-query-help/python/py-regex-duplicate-in-character-class/) | Maintainability | Warning | +| [First parameter of a class method is not named 'cls'](https://codeql.github.com/codeql-query-help/python/py-not-named-cls/) | Maintainability | Recommendation | +| [First parameter of a method is not named 'self'](https://codeql.github.com/codeql-query-help/python/py-not-named-self/) | Maintainability | Recommendation | +| [Implicit string concatenation in a list](https://codeql.github.com/codeql-query-help/python/py-implicit-string-concatenation-in-list/) | Maintainability | Warning | +| [Imprecise assert](https://codeql.github.com/codeql-query-help/python/py-imprecise-assert/) | Maintainability | Recommendation | +| [Module imports itself](https://codeql.github.com/codeql-query-help/python/py-import-own-module/) | Maintainability | Recommendation | +| [Module is imported more than once](https://codeql.github.com/codeql-query-help/python/py-repeated-import/) | Maintainability | Recommendation | +| [Module is imported with 'import' and 'import from'](https://codeql.github.com/codeql-query-help/python/py-import-and-import-from/) | Maintainability | Recommendation | +| [Nested loops with same variable](https://codeql.github.com/codeql-query-help/python/py-nested-loops-with-same-variable/) | Maintainability | Recommendation | +| [Overly complex `__del__` method](https://codeql.github.com/codeql-query-help/python/py-overly-complex-delete/) | Maintainability | Recommendation | +| [Redundant comparison](https://codeql.github.com/codeql-query-help/python/py-redundant-comparison/) | Maintainability | Warning | +| [Should use a 'with' statement](https://codeql.github.com/codeql-query-help/python/py-should-use-with/) | Maintainability | Recommendation | +| [Statement has no effect](https://codeql.github.com/codeql-query-help/python/py-ineffectual-statement/) | Maintainability | Recommendation | +| [Unnecessary 'else' clause in loop](https://codeql.github.com/codeql-query-help/python/py-redundant-else/) | Maintainability | Warning | +| [Unnecessary lambda](https://codeql.github.com/codeql-query-help/python/py-unnecessary-lambda/) | Maintainability | Recommendation | +| [Unnecessary pass](https://codeql.github.com/codeql-query-help/python/py-unnecessary-pass/) | Maintainability | Warning | +| [Unreachable code](https://codeql.github.com/codeql-query-help/python/py-unreachable-statement/) | Maintainability | Warning | +| [Unused argument in a formatting call](https://codeql.github.com/codeql-query-help/python/py-str-format-surplus-argument/) | Maintainability | Warning | +| [Unused global variable](https://codeql.github.com/codeql-query-help/python/py-unused-global-variable/) | Maintainability | Recommendation | +| [Unused import](https://codeql.github.com/codeql-query-help/python/py-unused-import/) | Maintainability | Recommendation | +| [Unused local variable](https://codeql.github.com/codeql-query-help/python/py-unused-local-variable/) | Maintainability | Recommendation | +| [Unused named argument in formatting call](https://codeql.github.com/codeql-query-help/python/py-str-format-surplus-named-argument/) | Maintainability | Warning | +| [Use of 'global' at module level](https://codeql.github.com/codeql-query-help/python/py-redundant-global-declaration/) | Maintainability | Warning | +| [Use of the return value of a procedure](https://codeql.github.com/codeql-query-help/python/py-procedure-return-value-used/) | Maintainability | Warning | +| [Variable defined multiple times](https://codeql.github.com/codeql-query-help/python/py-multiple-definition/) | Maintainability | Warning | +| [`__del__` is called explicitly](https://codeql.github.com/codeql-query-help/python/py-explicit-call-to-delete/) | Reliability | Warning | +| [`__eq__` not overridden when adding attributes](https://codeql.github.com/codeql-query-help/python/py-missing-equals/) | Reliability | Warning | +| [`__init__` method calls overridden method](https://codeql.github.com/codeql-query-help/python/py-init-calls-subclass/) | Reliability | Warning | +| [`__init__` method is a generator](https://codeql.github.com/codeql-query-help/python/py-init-method-is-generator/) | Reliability | Error | +| [`__init__` method returns a value](https://codeql.github.com/codeql-query-help/python/py-explicit-return-in-init/) | Reliability | Error | +| [`__iter__` method returns a non-iterator](https://codeql.github.com/codeql-query-help/python/py-iter-returns-non-iterator/) | Reliability | Error | +| [An assert statement has a side-effect](https://codeql.github.com/codeql-query-help/python/py-side-effect-in-assert/) | Reliability | Error | +| [Asserting a tuple](https://codeql.github.com/codeql-query-help/python/py-asserts-tuple/) | Reliability | Error | +| [Comparison using is when operands support `__eq__`](https://codeql.github.com/codeql-query-help/python/py-comparison-using-is/) | Reliability | Warning | +| [Conflicting attributes in base classes](https://codeql.github.com/codeql-query-help/python/py-conflicting-attributes/) | Reliability | Warning | +| [Empty except](https://codeql.github.com/codeql-query-help/python/py-empty-except/) | Reliability | Recommendation | +| [Encoding error](https://codeql.github.com/codeql-query-help/python/py-encoding-error/) | Reliability | Error | +| [Except block handles 'BaseException'](https://codeql.github.com/codeql-query-help/python/py-catch-base-exception/) | Reliability | Recommendation | +| [Explicit export is not defined](https://codeql.github.com/codeql-query-help/python/py-undefined-export/) | Reliability | Error | +| [Explicit returns mixed with implicit (fall through) returns](https://codeql.github.com/codeql-query-help/python/py-mixed-returns/) | Reliability | Recommendation | +| [File is not always closed](https://codeql.github.com/codeql-query-help/python/py-file-not-closed/) | Reliability | Warning | +| [First argument to super() is not enclosing class](https://codeql.github.com/codeql-query-help/python/py-super-not-enclosing-class/) | Reliability | Error | +| [Formatted object is not a mapping](https://codeql.github.com/codeql-query-help/python/py-percent-format-not-mapping/) | Reliability | Error | +| [Formatting string mixes implicitly and explicitly numbered fields](https://codeql.github.com/codeql-query-help/python/py-str-format-mixed-fields/) | Reliability | Error | +| [Illegal raise](https://codeql.github.com/codeql-query-help/python/py-illegal-raise/) | Reliability | Error | +| [Incomplete ordering](https://codeql.github.com/codeql-query-help/python/py-incomplete-ordering/) | Reliability | Warning | +| [Inconsistent equality and hashing](https://codeql.github.com/codeql-query-help/python/py-equals-hash-mismatch/) | Reliability | Warning | +| [Inconsistent equality and inequality](https://codeql.github.com/codeql-query-help/python/py-inconsistent-equality/) | Reliability | Warning | +| [Inconsistent method resolution order](https://codeql.github.com/codeql-query-help/python/py-inconsistent-mro/) | Reliability | Error | +| [Iterable can be either a string or a sequence](https://codeql.github.com/codeql-query-help/python/py-iteration-string-and-sequence/) | Reliability | Error | +| [Iterator does not return self from `__iter__` method](https://codeql.github.com/codeql-query-help/python/py-iter-returns-non-self/) | Reliability | Error | +| [Loop variable capture](https://codeql.github.com/codeql-query-help/python/py-loop-variable-capture/) | Reliability | Error | +| [Maybe missing 'self' in comparison](https://codeql.github.com/codeql-query-help/python/py-comparison-missing-self/) | Reliability | Warning | +| [Membership test with a non-container](https://codeql.github.com/codeql-query-help/python/py-member-test-non-container/) | Reliability | Error | +| [Mismatch between signature and use of an overridden method](https://codeql.github.com/codeql-query-help/python/py-inheritance-incorrect-overridden-signature/) | Reliability | Recommendation | +| [Mismatch between signature and use of an overriding method](https://codeql.github.com/codeql-query-help/python/py-inheritance-incorrect-overriding-signature/) | Reliability | Error | +| [Mismatch in multiple assignment](https://codeql.github.com/codeql-query-help/python/py-mismatched-multiple-assignment/) | Reliability | Error | +| [Missing call to superclass `__del__` during object destruction](https://codeql.github.com/codeql-query-help/python/py-missing-call-to-delete/) | Reliability | Error | +| [Missing call to superclass `__init__` during object initialization](https://codeql.github.com/codeql-query-help/python/py-missing-call-to-init/) | Reliability | Error | +| [Missing named arguments in formatting call](https://codeql.github.com/codeql-query-help/python/py-str-format-missing-named-argument/) | Reliability | Error | +| [Missing part of special group in regular expression](https://codeql.github.com/codeql-query-help/python/py-regex-incomplete-special-group/) | Reliability | Warning | +| [Modification of dictionary returned by locals()](https://codeql.github.com/codeql-query-help/python/py-modification-of-locals/) | Reliability | Warning | +| [Modification of parameter with default](https://codeql.github.com/codeql-query-help/python/py-modification-of-default-value/) | Reliability | Error | +| [Multiple calls to `__del__` during object destruction](https://codeql.github.com/codeql-query-help/python/py-multiple-calls-to-delete/) | Reliability | Warning | +| [Multiple calls to `__init__` during object initialization](https://codeql.github.com/codeql-query-help/python/py-multiple-calls-to-init/) | Reliability | Warning | +| [Mutation of descriptor in `__get__` or `__set__` method](https://codeql.github.com/codeql-query-help/python/py-mutable-descriptor/) | Reliability | Error | +| [Nested loops with same variable reused after inner loop body](https://codeql.github.com/codeql-query-help/python/py-nested-loops-with-same-variable-reused/) | Reliability | Error | +| [Non-callable called](https://codeql.github.com/codeql-query-help/python/py-call-to-non-callable/) | Reliability | Error | +| [Non-exception in 'except' clause](https://codeql.github.com/codeql-query-help/python/py-useless-except/) | Reliability | Error | +| [Non-iterable used in for loop](https://codeql.github.com/codeql-query-help/python/py-non-iterable-in-for-loop/) | Reliability | Error | +| [Non-standard exception raised in special method](https://codeql.github.com/codeql-query-help/python/py-unexpected-raise-in-special-method/) | Reliability | Recommendation | +| [Raising `NotImplemented`](https://codeql.github.com/codeql-query-help/python/py-raise-not-implemented/) | Reliability | Warning | +| [Redundant assignment](https://codeql.github.com/codeql-query-help/python/py-redundant-assignment/) | Reliability | Error | +| [Returning tuples with varying lengths](https://codeql.github.com/codeql-query-help/python/py-mixed-tuple-returns/) | Reliability | Recommendation | +| [Signature mismatch in overriding method](https://codeql.github.com/codeql-query-help/python/py-inheritance-signature-mismatch/) | Reliability | Warning | +| [Special method has incorrect signature](https://codeql.github.com/codeql-query-help/python/py-special-method-wrong-signature/) | Reliability | Error | +| [Superclass attribute shadows subclass method](https://codeql.github.com/codeql-query-help/python/py-attribute-shadows-method/) | Reliability | Error | +| [Suspicious unused loop iteration variable](https://codeql.github.com/codeql-query-help/python/py-unused-loop-variable/) | Reliability | Error | +| [Syntax error](https://codeql.github.com/codeql-query-help/python/py-syntax-error/) | Reliability | Error | +| [Testing equality to None](https://codeql.github.com/codeql-query-help/python/py-test-equals-none/) | Reliability | Recommendation | +| [Too few arguments in formatting call](https://codeql.github.com/codeql-query-help/python/py-str-format-missing-argument/) | Reliability | Error | +| [Unhashable object hashed](https://codeql.github.com/codeql-query-help/python/py-hash-unhashable-value/) | Reliability | Error | +| [Unmatchable caret in regular expression](https://codeql.github.com/codeql-query-help/python/py-regex-unmatchable-caret/) | Reliability | Error | +| [Unmatchable dollar in regular expression](https://codeql.github.com/codeql-query-help/python/py-regex-unmatchable-dollar/) | Reliability | Error | +| [Unreachable `except` block](https://codeql.github.com/codeql-query-help/python/py-unreachable-except/) | Reliability | Error | +| [Unsupported format character](https://codeql.github.com/codeql-query-help/python/py-percent-format-unsupported-character/) | Reliability | Error | +| [Unused exception object](https://codeql.github.com/codeql-query-help/python/py-unused-exception-object/) | Reliability | Error | +| [Use of a print statement at module level](https://codeql.github.com/codeql-query-help/python/py-print-during-import/) | Reliability | Recommendation | +| [Use of exit() or quit()](https://codeql.github.com/codeql-query-help/python/py-use-of-exit-or-quit/) | Reliability | Warning | +| [Wrong name for an argument in a call](https://codeql.github.com/codeql-query-help/python/py-call-wrong-named-argument/) | Reliability | Error | +| [Wrong name for an argument in a class instantiation](https://codeql.github.com/codeql-query-help/python/py-call-wrong-named-class-argument/) | Reliability | Error | +| [Wrong number of arguments for format](https://codeql.github.com/codeql-query-help/python/py-percent-format-wrong-arguments/) | Reliability | Error | +| [Wrong number of arguments in a call](https://codeql.github.com/codeql-query-help/python/py-call-wrong-arguments/) | Reliability | Error | +| [Wrong number of arguments in a class instantiation](https://codeql.github.com/codeql-query-help/python/py-call-wrong-number-class-arguments/) | Reliability | Error | + +{% endrowheaders %} diff --git a/data/reusables/code-quality/codeql-query-tables/ruby.md b/data/reusables/code-quality/codeql-query-tables/ruby.md new file mode 100644 index 000000000000..9e1532753e90 --- /dev/null +++ b/data/reusables/code-quality/codeql-query-tables/ruby.md @@ -0,0 +1,9 @@ +{% rowheaders %} + +| Query name | Category | Severity | +| --- | --- | --- | +| [Useless assignment to local variable](https://codeql.github.com/codeql-query-help/ruby/rb-useless-assignment-to-local/) | Maintainability | Warning | +| [Database query in a loop](https://codeql.github.com/codeql-query-help/ruby/rb-database-query-in-loop/) | Reliability | Info | +| [Potentially uninitialized local variable](https://codeql.github.com/codeql-query-help/ruby/rb-uninitialized-local-variable/) | Reliability | Error | + +{% endrowheaders %} diff --git a/data/reusables/code-quality/codeql-query-tables/rust.md b/data/reusables/code-quality/codeql-query-tables/rust.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/eslint.config.ts b/eslint.config.ts index c0937d6df79c..510b63d1c776 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -102,13 +102,15 @@ export default [ }, // Ignored patterns + // CodeQL scripts included because cocofix is install manually by the workflow { ignores: [ 'tmp/*', '.next/', 'rest-api-description/', 'docs-internal-data/', - 'src/code-scanning/scripts/generate-code-scanning-query-list.ts', + 'src/codeql-queries/scripts/generate-code-scanning-query-list.ts', + 'src/codeql-queries/scripts/generate-code-quality-query-list.ts', 'next-env.d.ts', ], }, diff --git a/package.json b/package.json index db07b9267bed..250be69b5fbf 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "lint-translation": "vitest src/content-linter/tests/lint-files.ts", "liquid-markdown-tables": "tsx src/tools/scripts/liquid-markdown-tables/index.ts", "generate-article-api-docs": "tsx src/article-api/scripts/generate-api-docs.ts", - "generate-code-scanning-query-list": "tsx src/code-scanning/scripts/generate-code-scanning-query-list.ts", + "generate-code-scanning-query-list": "tsx src/codeql-queries/scripts/generate-code-scanning-query-list.ts", + "generate-code-quality-query-list": "tsx src/codeql-queries/scripts/generate-code-quality-query-list.ts", "generate-content-linter-docs": "tsx src/content-linter/scripts/generate-docs.ts", "move-content": "tsx src/content-render/scripts/move-content.ts", "openapi-docs": "tsx src/rest/docs.ts", @@ -77,6 +78,7 @@ "release-banner": "tsx src/ghes-releases/scripts/release-banner.ts", "repo-sync": "./src/workflows/local-repo-sync.sh", "reusables": "tsx src/content-render/scripts/reusables-cli.ts", + "resolve-liquid": "tsx src/content-render/scripts/resolve-liquid.ts", "rendered-content-link-checker": "tsx src/links/scripts/rendered-content-link-checker.ts", "rendered-content-link-checker-cli": "tsx src/links/scripts/rendered-content-link-checker-cli.ts", "rest-dev": "tsx src/rest/scripts/update-files.ts", diff --git a/src/article-api/README.md b/src/article-api/README.md index 28c749bd6b02..38d94318f7cb 100644 --- a/src/article-api/README.md +++ b/src/article-api/README.md @@ -23,7 +23,12 @@ The `/api/article` endpoints return information about a page by `pathname`. ### Autogenerated Content Transformers -For autogenerated pages (REST, landing pages, audit logs, webhooks, GraphQL, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture: +For autogenerated pages (REST, GraphQL, webhooks, landing pages, audit logs, etc), the Article API uses specialized transformers to convert the rendered content into markdown format. These transformers are located in `src/article-api/transformers/` and use an extensible architecture: + +#### Current Transformers + +- **REST Transformer** (`rest-transformer.ts`) - Converts REST API operations into markdown, including endpoints, parameters, status codes, and code examples +- **GraphQL Transformer** (`graphql-transformer.ts`) - Converts GraphQL schema documentation into markdown, including queries, mutations, objects, interfaces, enums, unions, input objects, scalars, changelog, and breaking changes To add a new transformer for other autogenerated content types: 1. Create a new transformer file implementing the `PageTransformer` interface diff --git a/src/article-api/templates/graphql-breaking-changes.template.md b/src/article-api/templates/graphql-breaking-changes.template.md new file mode 100644 index 000000000000..5bfa49a9d0f6 --- /dev/null +++ b/src/article-api/templates/graphql-breaking-changes.template.md @@ -0,0 +1,21 @@ +# {{ pageTitle }} + +{{ pageIntro }} + +{{ manualContent }} + +{% for change in breakingChangesByDate %} + +## {{ change.heading }} + +{% for item in change.items %} + +- {% if item.criticality == 'breaking' %}**Breaking**{% else %}**Dangerous**{% endif %} A change will be made to `{{ item.location }}`. + + **Description:** {{ item.description }} + + **Reason:** {{ item.reason }} + +{% endfor %} + +{% endfor %} diff --git a/src/article-api/templates/graphql-changelog.template.md b/src/article-api/templates/graphql-changelog.template.md new file mode 100644 index 000000000000..b1461930710f --- /dev/null +++ b/src/article-api/templates/graphql-changelog.template.md @@ -0,0 +1,38 @@ +# {{ pageTitle }} + +{{ pageIntro }} + +{{ manualContent }} + +{% for item in changelogItems %} + +## Schema changes for {{ item.date }} + +{% for schemaChange in item.schemaChanges %} + +### {{ schemaChange.title }} + +{% for change in schemaChange.changes %}- {{ change }} +{% endfor %} + +{% endfor %} + +{% for previewChange in item.previewChanges %} + +### {{ previewChange.title }} + +{% for change in previewChange.changes %}- {{ change }} +{% endfor %} + +{% endfor %} + +{% for upcomingChange in item.upcomingChanges %} + +### {{ upcomingChange.title }} + +{% for change in upcomingChange.changes %}- {{ change }} +{% endfor %} + +{% endfor %} + +{% endfor %} diff --git a/src/article-api/templates/graphql-index.template.md b/src/article-api/templates/graphql-index.template.md new file mode 100644 index 000000000000..dd2132c8d938 --- /dev/null +++ b/src/article-api/templates/graphql-index.template.md @@ -0,0 +1,9 @@ +# {{ pageTitle }} + +{{ pageIntro }} + +{{ manualContent }} + +## Reference pages + +{{ childrenLinks }} diff --git a/src/article-api/templates/graphql-reference.template.md b/src/article-api/templates/graphql-reference.template.md new file mode 100644 index 000000000000..955db8e5f295 --- /dev/null +++ b/src/article-api/templates/graphql-reference.template.md @@ -0,0 +1,120 @@ +# {{ pageTitle }} + +{{ pageIntro }} + +{{ manualContent }} + +{% for item in items %} + +## {{ item.name }} + +{{ item.description }} + +{% if item.isDeprecated %} + +> [!WARNING] +> **Deprecation notice:** {{ item.deprecationReason }} +{% endif %} + +{% if pageType == 'queries' %} +**Type:** [{{ item.type }}]({{ item.href }}) + +{% if item.args.size > 0 %} + +### Arguments for `{{ item.name }}` + +| Name | Type | Description | +| --- | --- | --- | +{% for arg in item.args %}| `{{ arg.name }}` | [`{{ arg.type }}`]({{ arg.href }}) | {{ arg.description }} | +{% endfor %} +{% endif %} + +{% elsif pageType == 'mutations' %} +{% if item.inputFields.size > 0 %} + +### Input fields for `{{ item.name }}` + +| Name | Type | Description | +| --- | --- | --- | +{% for field in item.inputFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %} | +{% endfor %} +{% endif %} + +{% if item.returnFields.size > 0 %} + +### Return fields for `{{ item.name }}` + +| Name | Type | Description | +| --- | --- | --- | +{% for field in item.returnFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %} | +{% endfor %} +{% endif %} + +{% elsif pageType == 'objects' %} +{% if item.implements.size > 0 %} + +### Implements + +{% for impl in item.implements %}- [`{{ impl.name }}`]({{ impl.href }}) +{% endfor %} +{% endif %} + +{% if item.fields.size > 0 %} + +### Fields for `{{ item.name }}` + +| Name | Type | Description | +| --- | --- | --- | +{% for field in item.fields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}{% if field.arguments.size > 0 %}

**Arguments:**
{% for arg in field.arguments %}- `{{ arg.name }}` ([`{{ arg.type.name }}`]({{ arg.type.href }})): {{ arg.description }}{% if arg.defaultValue %} Default: `{{ arg.defaultValue }}`.{% endif %}
{% endfor %}{% endif %} | +{% endfor %} +{% endif %} + +{% elsif pageType == 'interfaces' %} +{% if item.fields.size > 0 %} + +### Fields for `{{ item.name }}` + +| Name | Type | Description | +| --- | --- | --- | +{% for field in item.fields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %}{% if field.arguments.size > 0 %}

**Arguments:**
{% for arg in field.arguments %}- `{{ arg.name }}` ([`{{ arg.type.name }}`]({{ arg.type.href }})): {{ arg.description }}{% if arg.defaultValue %} Default: `{{ arg.defaultValue }}`.{% endif %}
{% endfor %}{% endif %} | +{% endfor %} +{% endif %} + +{% elsif pageType == 'enums' %} +{% if item.values.size > 0 %} + +### Values for `{{ item.name }}` + +{% for value in item.values %}**`{{ value.name }}`** + +{{ value.description }} + +{% endfor %} +{% endif %} + +{% elsif pageType == 'unions' %} +{% if item.possibleTypes.size > 0 %} + +### Possible types for `{{ item.name }}` + +{% for type in item.possibleTypes %}- [`{{ type.name }}`]({{ type.href }}) +{% endfor %} +{% endif %} + +{% elsif pageType == 'inputObjects' %} +{% if item.inputFields.size > 0 %} + +### Input fields for `{{ item.name }}` + +| Name | Type | Description | +| --- | --- | --- | +{% for field in item.inputFields %}| `{{ field.name }}` | [`{{ field.type }}`]({{ field.href }}) | {{ field.description }}{% if field.defaultValue %} Default: `{{ field.defaultValue }}`.{% endif %}{% if field.isDeprecated %} **Deprecated:** {{ field.deprecationReason }}{% endif %} | +{% endfor %} +{% endif %} + +{% elsif pageType == 'scalars' %} +{%- comment -%}Scalars typically just have name and description{%- endcomment -%} + +{% endif %} + +{% endfor %} diff --git a/src/article-api/tests/graphql-transformer.ts b/src/article-api/tests/graphql-transformer.ts new file mode 100644 index 000000000000..8b2dda536253 --- /dev/null +++ b/src/article-api/tests/graphql-transformer.ts @@ -0,0 +1,370 @@ +import { beforeAll, describe, expect, test } from 'vitest' + +import { get } from '@/tests/helpers/e2etest' + +const makeURL = (pathname: string): string => { + const params = new URLSearchParams({ pathname }) + return `/api/article/body?${params}` +} + +describe('GraphQL transformer', { timeout: 10000 }, () => { + // Cache expensive responses to avoid duplicate requests + const responseCache = new Map>>() + + const getCached = async (url: string) => { + if (!responseCache.has(url)) { + responseCache.set(url, await get(makeURL(url))) + } + return responseCache.get(url)! + } + + beforeAll(() => { + if (!process.env.ROOT) { + console.warn( + 'WARNING: The GraphQL transformer tests require the ROOT environment variable to be set to the fixture root', + ) + } + }) + + describe('Reference pages', () => { + test('queries page renders with markdown structure', async () => { + const res = await getCached('/en/graphql/reference/queries') + expect(res.statusCode).toBe(200) + expect(res.headers['content-type']).toContain('text/markdown') + + // Check for the main heading + expect(res.body).toContain('# Queries') + + // Check for intro + expect(res.body).toContain( + 'The query type defines GraphQL operations that retrieve data from the server.', + ) + + // Check for manual content section + expect(res.body).toContain('## About queries') + expect(res.body).toContain('Every GraphQL schema has a root type') + }) + + test('queries are formatted correctly', async () => { + const res = await getCached('/en/graphql/reference/queries') + expect(res.statusCode).toBe(200) + + // Check for query heading + expect(res.body).toContain('## repository') + + // Check for query description + expect(res.body).toContain('Lookup a given repository by the owner and repository name.') + + // Check for type link + expect(res.body).toContain('**Type:** [Repository](/en/graphql/reference/objects#repository)') + }) + + test('query arguments are listed in table format', async () => { + const res = await getCached('/en/graphql/reference/queries') + expect(res.statusCode).toBe(200) + + // Check for arguments table for codeOfConduct query + expect(res.body).toContain('### Arguments for `codeOfConduct`') + expect(res.body).toMatch(/\|\s*Name\s*\|\s*Type\s*\|\s*Description\s*\|/) + expect(res.body).toMatch(/\|\s*-+\s*\|\s*-+\s*\|\s*-+\s*\|/) + + // Check for specific arguments + expect(res.body).toMatch(/\|\s*`key`\s*\|/) + expect(res.body).toContain('[`String!`](/en/graphql/reference/scalars#string)') + expect(res.body).toContain("The code of conduct's key.") + }) + + test('mutations page renders correctly', async () => { + const res = await getCached('/en/graphql/reference/mutations') + expect(res.statusCode).toBe(200) + + // Check for mutation heading + expect(res.body).toContain('## createRepository') + + // Check for mutation description + expect(res.body).toContain('Create a new repository.') + + // Check for input fields table + expect(res.body).toContain('### Input fields for `createRepository`') + expect(res.body).toContain('| `input` |') + + // Check for return fields table + expect(res.body).toContain('### Return fields for `createRepository`') + expect(res.body).toMatch(/\|\s*`repository`\s*\|/) + expect(res.body).toContain('The new repository.') + }) + + test('objects page renders with implements and fields', async () => { + const res = await getCached('/en/graphql/reference/objects') + expect(res.statusCode).toBe(200) + + // Check for object heading - AddedToMergeQueueEvent has implements + expect(res.body).toContain('## AddedToMergeQueueEvent') + + // Check for implements section + expect(res.body).toContain('### Implements') + expect(res.body).toMatch(/[*-]\s*\[`Node`\]\(\/.*graphql\/reference\/interfaces#node\)/) + + // Check for fields table + expect(res.body).toContain('### Fields for `AddedToMergeQueueEvent`') + expect(res.body).toMatch(/\|\s*`id`\s*\|/) + expect(res.body).toMatch(/\|\s*`actor`\s*\|/) + expect(res.body).toMatch(/\|\s*`createdAt`\s*\|/) + }) + + test('objects page shows field arguments inline', async () => { + const res = await getCached('/en/graphql/reference/objects') + expect(res.statusCode).toBe(200) + + // Check for User object with repositories field that has arguments + expect(res.body).toContain('## User') + expect(res.body).toContain('| `repositories` |') + + // Check for inline arguments formatting + expect(res.body).toContain('**Arguments:**') + expect(res.body).toContain('- `first`') + expect(res.body).toContain('Returns the first n elements from the list.') + expect(res.body).toContain('- `orderBy`') + }) + + test('interfaces page renders correctly', async () => { + const res = await getCached('/en/graphql/reference/interfaces') + expect(res.statusCode).toBe(200) + + // Check for interface heading + expect(res.body).toContain('## Node') + + // Check for interface description + expect(res.body).toContain('An object with an ID.') + + // Check for fields table + expect(res.body).toContain('### Fields for `Node`') + expect(res.body).toContain('| `id` |') + expect(res.body).toContain('ID of the object.') + }) + + test('enums page renders with values', async () => { + const res = await getCached('/en/graphql/reference/enums') + expect(res.statusCode).toBe(200) + + // Check for enum heading + expect(res.body).toContain('## RepositoryVisibility') + + // Check for enum description + expect(res.body).toContain("The repository's visibility level.") + + // Check for values section + expect(res.body).toContain('### Values for `RepositoryVisibility`') + expect(res.body).toContain('**`PUBLIC`**') + expect(res.body).toContain('The repository is visible to everyone.') + expect(res.body).toContain('**`PRIVATE`**') + expect(res.body).toContain('The repository is visible only to those with explicit access.') + expect(res.body).toContain('**`INTERNAL`**') + }) + + test('unions page renders with possible types', async () => { + const res = await getCached('/en/graphql/reference/unions') + expect(res.statusCode).toBe(200) + + // Check for union heading + expect(res.body).toContain('## SearchResultItem') + + // Check for union description + expect(res.body).toContain('The results of a search.') + + // Check for possible types + expect(res.body).toContain('### Possible types for `SearchResultItem`') + expect(res.body).toMatch(/[*-]\s*\[`Bot`\]\(\/.*graphql\/reference\/objects#bot\)/) + expect(res.body).toMatch( + /[*-]\s*\[`PullRequest`\]\(\/.*graphql\/reference\/objects#pullrequest\)/, + ) + expect(res.body).toMatch(/[*-]\s*\[`User`\]\(\/.*graphql\/reference\/objects#user\)/) + }) + + test('input-objects page renders correctly', async () => { + const res = await getCached('/en/graphql/reference/input-objects') + expect(res.statusCode).toBe(200) + + // Check for input object heading + expect(res.body).toContain('## AbortQueuedMigrationsInput') + + // Check for input object description + expect(res.body).toContain('Autogenerated input type of CreateRepository.') + + // Check for input fields table + expect(res.body).toContain('### Input fields for `AbortQueuedMigrationsInput`') + expect(res.body).toMatch(/\|\s*`ownerId`\s*\|/) + expect(res.body).toContain('The ID of the organization that is running the migrations.') + }) + + test('scalars page renders correctly', async () => { + const res = await getCached('/en/graphql/reference/scalars') + expect(res.statusCode).toBe(200) + + // Check for scalar heading + expect(res.body).toContain('## Boolean') + + // Check for scalar description + expect(res.body).toContain('Represents true or false values.') + + // Check for other scalars + expect(res.body).toContain('## String') + expect(res.body).toContain('## ID') + expect(res.body).toContain('## Int') + }) + + test('reference index page renders', async () => { + const res = await getCached('/en/graphql/reference') + expect(res.statusCode).toBe(200) + + // Check for main heading + expect(res.body).toContain('# Reference') + + // Check for intro with liquid variable rendered + expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API schema/) + }) + }) + + describe('Overview pages', () => { + test('changelog page renders with changes', async () => { + const res = await getCached('/en/graphql/overview/changelog') + expect(res.statusCode).toBe(200) + + // Check for main heading + expect(res.body).toContain('# Changelog') + + // Check for intro + expect(res.body).toContain( + 'The GraphQL schema changelog is a list of recent and upcoming changes', + ) + + // Check for manual content + expect(res.body).toContain( + 'Breaking changes include changes that will break existing queries', + ) + + // Check for date-based changelog sections + expect(res.body).toContain('## Schema changes for 2025-11-30') + + // Check for change items + expect(res.body).toContain('### The GraphQL schema includes these changes:') + expect(res.body).toContain('Type SuggestedReviewerActor was added') + }) + + test('changelog removes HTML tags from changes', async () => { + const res = await getCached('/en/graphql/overview/changelog') + expect(res.statusCode).toBe(200) + + // Check that HTML tags are removed + expect(res.body).toContain('Field suggestedReviewerActors was added') + expect(res.body).not.toContain('') + expect(res.body).not.toContain('') + expect(res.body).not.toContain('

') + expect(res.body).not.toContain('

') + }) + + test('breaking changes page renders with scheduled changes', async () => { + const res = await getCached('/en/graphql/overview/breaking-changes') + expect(res.statusCode).toBe(200) + + // Check for main heading + expect(res.body).toContain('# Breaking changes') + + // Check for intro + expect(res.body).toContain('Learn about recent and upcoming breaking changes') + + // Check for manual content + expect(res.body).toContain('## About breaking changes') + expect(res.body).toContain('Breaking:** Changes that will break existing queries') + + // Check for date-based sections + expect(res.body).toContain('## Changes scheduled for 2025-04-01') + expect(res.body).toContain('## Changes scheduled for 2026-04-01') + }) + + test('breaking changes shows criticality levels', async () => { + const res = await getCached('/en/graphql/overview/breaking-changes') + expect(res.statusCode).toBe(200) + + // Check for breaking criticality + expect(res.body).toMatch(/\*\*Breaking\*\*\s+A change will be made to `\w+\.\w+`\./) + expect(res.body).toMatch(/\*\*Description:\*\*.*will be removed/) + expect(res.body).toMatch(/\*\*Reason:\*\*/) + }) + + test('breaking changes removes HTML tags', async () => { + const res = await getCached('/en/graphql/overview/breaking-changes') + expect(res.statusCode).toBe(200) + expect(res.body).toContain('scheduled for') + + // Check that HTML tags are removed from descriptions + expect(res.body).not.toContain('

') + expect(res.body).not.toContain('

') + expect(res.body).not.toContain('') + expect(res.body).not.toContain('') + expect(res.body).not.toContain('

') + expect(res.body).not.toContain('

') + }) + }) + + describe('Liquid tags', () => { + test('AUTOTITLE links are resolved in manual content', async () => { + const res = await getCached('/en/graphql/reference/queries') + expect(res.statusCode).toBe(200) + + // Check that AUTOTITLE has been resolved + expect(res.body).toMatch(/(Forming calls with GraphQL|Hello World)/) + expect(res.body).toContain('(/en/get-started/start-your-journey/hello-world)') + + // Make sure the raw AUTOTITLE tag is not present + expect(res.body).not.toContain('[AUTOTITLE]') + }) + + test('Liquid variables are rendered in intro', async () => { + const res = await getCached('/en/graphql/reference') + expect(res.statusCode).toBe(200) + + // Liquid variables should be rendered + expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API schema/) + expect(res.body).not.toContain('{% data variables.product.prodname_dotcom %}') + }) + + test('Liquid variables are rendered in breaking changes', async () => { + const res = await getCached('/en/graphql/overview/breaking-changes') + expect(res.statusCode).toBe(200) + + // Check that liquid variables in intro are rendered + expect(res.body).toMatch(/(GitHub|HubGit) GraphQL API/) + expect(res.body).not.toContain('{% data variables.product.prodname_dotcom %}') + }) + }) + + describe('Multiple items', () => { + test('multiple queries are all rendered', async () => { + const res = await getCached('/en/graphql/reference/queries') + expect(res.statusCode).toBe(200) + + // Check for multiple query headings + expect(res.body).toContain('## repository') + expect(res.body).toContain('## viewer') + }) + + test('multiple objects are all rendered', async () => { + const res = await getCached('/en/graphql/reference/objects') + expect(res.statusCode).toBe(200) + + // Check for multiple object headings + expect(res.body).toContain('## Repository') + expect(res.body).toContain('## User') + }) + + test('multiple enums are all rendered', async () => { + const res = await getCached('/en/graphql/reference/enums') + expect(res.statusCode).toBe(200) + + // Check for multiple enum headings + expect(res.body).toContain('## RepositoryVisibility') + expect(res.body).toContain('## OrderDirection') + }) + }) +}) diff --git a/src/article-api/transformers/graphql-transformer.ts b/src/article-api/transformers/graphql-transformer.ts new file mode 100644 index 000000000000..12a2c43a049d --- /dev/null +++ b/src/article-api/transformers/graphql-transformer.ts @@ -0,0 +1,478 @@ +import type { Context, Page } from '@/types' +import type { PageTransformer } from './types' +import type { + QueryT, + MutationT, + ObjectT, + InterfaceT, + EnumT, + UnionT, + InputObjectT, + ScalarT, + ChangelogItemT, + BreakingChangesT, + FieldT, +} from '@/graphql/components/types' +import { renderContent } from '@/content-render/index' +import matter from '@gr2m/gray-matter' +import { readFileSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import { fastTextOnly } from '@/content-render/unified/text-only' +import GithubSlugger from 'github-slugger' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +/** + * Transformer for GraphQL pages + * Converts GraphQL schema data into markdown format using Liquid templates + */ +export class GraphQLTransformer implements PageTransformer { + canTransform(page: Page): boolean { + return page.autogenerated === 'graphql' + } + + async transform(page: Page, pathname: string, context: Context): Promise { + const currentVersion = context.currentVersion! + + // Determine the page type from the pathname + const pathParts = pathname.split('/').filter(Boolean) + const graphqlIndex = pathParts.indexOf('graphql') + + if (graphqlIndex === -1) { + throw new Error(`Invalid GraphQL path: ${pathname}`) + } + + const section = pathParts[graphqlIndex + 1] // 'reference' or 'overview' + const pageType = pathParts[graphqlIndex + 2] // specific page like 'queries', 'changelog', etc. + + // Handle different GraphQL page types + if (section === 'overview' && pageType === 'changelog') { + return await this.transformChangelog(page, currentVersion, context) + } else if (section === 'overview' && pageType === 'breaking-changes') { + return await this.transformBreakingChanges(page, currentVersion, context) + } else if (section === 'reference' && pageType) { + return await this.transformReference(page, currentVersion, context, pageType) + } else if (section === 'reference' && !pageType) { + // Index page - just render the intro and manual content + return await this.transformIndexPage(page, context) + } + + throw new Error(`Unsupported GraphQL page type: ${pathname}`) + } + + /** + * Transform the GraphQL reference index page + */ + private async transformIndexPage(page: Page, context: Context): Promise { + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + + const manualContent = await this.extractManualContent(page, context) + + // Get children links from page metadata + const children = page.children || [] + const childrenLinks = children + .map((child) => { + const childPath = child.startsWith('/') ? child : `/${child}` + const childName = childPath.split('/').pop() || '' + const displayName = childName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + return `- [${displayName}](${childPath})` + }) + .join('\n') + + const templateData = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + childrenLinks, + } + + const templatePath = join(__dirname, '../templates/graphql-index.template.md') + const templateContent = readFileSync(templatePath, 'utf8') + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } + + /** + * Transform GraphQL reference pages (queries, mutations, objects, etc.) + */ + private async transformReference( + page: Page, + currentVersion: string, + context: Context, + pageType: string, + ): Promise { + // Import GraphQL data functions dynamically + const { getGraphqlSchema } = await import('@/graphql/lib/index') + + // Map URL-friendly page type to internal schema key + const schemaKey = pageType === 'input-objects' ? 'inputObjects' : pageType + + const schema = getGraphqlSchema(currentVersion, schemaKey) + + // Prepare intro and manual content + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + const manualContent = await this.extractManualContent(page, context) + + // Prepare the schema items based on page type + let preparedItems: Array> = [] + + switch (schemaKey) { + case 'queries': + preparedItems = await Promise.all( + (schema as QueryT[]).map((item) => this.prepareQuery(item)), + ) + break + case 'mutations': + preparedItems = await Promise.all( + (schema as MutationT[]).map((item) => this.prepareMutation(item)), + ) + break + case 'objects': + preparedItems = await Promise.all( + (schema as ObjectT[]).map((item) => this.prepareObject(item)), + ) + break + case 'interfaces': + preparedItems = await Promise.all( + (schema as InterfaceT[]).map((item) => this.prepareInterface(item)), + ) + break + case 'enums': + preparedItems = await Promise.all((schema as EnumT[]).map((item) => this.prepareEnum(item))) + break + case 'unions': + preparedItems = await Promise.all( + (schema as UnionT[]).map((item) => this.prepareUnion(item)), + ) + break + case 'inputObjects': + preparedItems = await Promise.all( + (schema as InputObjectT[]).map((item) => this.prepareInputObject(item)), + ) + break + case 'scalars': + preparedItems = await Promise.all( + (schema as ScalarT[]).map((item) => this.prepareScalar(item)), + ) + break + } + + const templateData = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + items: preparedItems, + pageType: schemaKey, + } + + const templatePath = join(__dirname, '../templates/graphql-reference.template.md') + const templateContent = readFileSync(templatePath, 'utf8') + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } + + /** + * Transform changelog page + */ + private async transformChangelog( + page: Page, + currentVersion: string, + context: Context, + ): Promise { + const { getGraphqlChangelog } = await import('@/graphql/lib/index') + + const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[] + + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + const manualContent = await this.extractManualContent(page, context) + + // Process changelog items + const changelogItems = schema.map((item) => { + const processChanges = (changes: Array<{ title: string; changes: string[] }>) => + changes.map((change) => ({ + title: change.title, + changes: change.changes.map((html: string) => { + // Remove wrapping

tags if present + if (html.startsWith('

') && html.endsWith('

')) { + return fastTextOnly(html.slice(3, -4)) + } + return fastTextOnly(html) + }), + })) + + return { + date: item.date, + schemaChanges: processChanges(item.schemaChanges || []), + previewChanges: processChanges(item.previewChanges || []), + upcomingChanges: processChanges(item.upcomingChanges || []), + } + }) + + const templateData = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + changelogItems, + } + + const templatePath = join(__dirname, '../templates/graphql-changelog.template.md') + const templateContent = readFileSync(templatePath, 'utf8') + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } + + /** + * Transform breaking changes page + */ + private async transformBreakingChanges( + page: Page, + currentVersion: string, + context: Context, + ): Promise { + const { getGraphqlBreakingChanges } = await import('@/graphql/lib/index') + + const schema = getGraphqlBreakingChanges(currentVersion) as BreakingChangesT + + const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' + const manualContent = await this.extractManualContent(page, context) + + const slugger = new GithubSlugger() + + // Process breaking changes by date + const breakingChangesByDate = Object.keys(schema).map((date) => { + const items = schema[date] + const heading = `Changes scheduled for ${date}` + const slug = slugger.slug(heading) + + return { + date, + heading, + slug, + items: items.map((item) => ({ + location: item.location, + description: fastTextOnly(item.description), + reason: fastTextOnly(item.reason), + criticality: item.criticality, + })), + } + }) + + const templateData = { + pageTitle: page.title, + pageIntro: intro, + manualContent, + breakingChangesByDate, + } + + const templatePath = join(__dirname, '../templates/graphql-breaking-changes.template.md') + const templateContent = readFileSync(templatePath, 'utf8') + + return await renderContent(templateContent, { + ...context, + ...templateData, + markdownRequested: true, + }) + } + + /** + * Extract manual content from page markdown + */ + private async extractManualContent(page: Page, context: Context): Promise { + if (!page.markdown) return '' + + const markerIndex = page.markdown.indexOf( + '', + ) + + if (markerIndex <= 0) return '' + + const { content } = matter(page.markdown) + const manualContentMarkerIndex = content.indexOf( + '', + ) + + if (manualContentMarkerIndex <= 0) return '' + + const rawManualContent = content.substring(0, manualContentMarkerIndex).trim() + if (!rawManualContent) return '' + + return await renderContent(rawManualContent, { + ...context, + markdownRequested: true, + }) + } + + /** + * Prepare a query item for rendering + */ + private async prepareQuery(query: QueryT): Promise> { + return { + name: query.name, + slug: query.name.toLowerCase(), + description: query.description ? fastTextOnly(query.description) : '', + type: query.type, + href: query.href, + isDeprecated: query.isDeprecated || false, + deprecationReason: query.deprecationReason + ? fastTextOnly(query.deprecationReason) + : undefined, + args: query.args.map((arg) => ({ + name: arg.name, + type: arg.type, + href: arg.href, + description: arg.description ? fastTextOnly(arg.description) : '', + })), + } + } + + /** + * Prepare a mutation item for rendering + */ + private async prepareMutation(mutation: MutationT): Promise> { + return { + name: mutation.name, + slug: mutation.name.toLowerCase(), + description: mutation.description ? fastTextOnly(mutation.description) : '', + isDeprecated: mutation.isDeprecated || false, + deprecationReason: mutation.deprecationReason + ? fastTextOnly(mutation.deprecationReason) + : undefined, + inputFields: await this.prepareFields(mutation.inputFields), + returnFields: await this.prepareFields(mutation.returnFields), + } + } + + /** + * Prepare an object item for rendering + */ + private async prepareObject(object: ObjectT): Promise> { + return { + name: object.name, + slug: object.name.toLowerCase(), + description: object.description ? fastTextOnly(object.description) : '', + isDeprecated: object.isDeprecated || false, + deprecationReason: object.deprecationReason + ? fastTextOnly(object.deprecationReason) + : undefined, + implements: object.implements || [], + fields: await this.prepareFields(object.fields), + } + } + + /** + * Prepare an interface item for rendering + */ + private async prepareInterface(item: InterfaceT): Promise> { + return { + name: item.name, + slug: item.name.toLowerCase(), + description: item.description ? fastTextOnly(item.description) : '', + isDeprecated: item.isDeprecated || false, + deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined, + fields: await this.prepareFields(item.fields), + } + } + + /** + * Prepare an enum item for rendering + */ + private async prepareEnum(item: EnumT): Promise> { + return { + name: item.name, + slug: item.name.toLowerCase(), + description: item.description ? fastTextOnly(item.description) : '', + isDeprecated: item.isDeprecated || false, + deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined, + values: item.values.map((value) => ({ + name: value.name, + description: value.description ? fastTextOnly(value.description) : '', + })), + } + } + + /** + * Prepare a union item for rendering + */ + private async prepareUnion(item: UnionT): Promise> { + return { + name: item.name, + slug: item.name.toLowerCase(), + description: item.description ? fastTextOnly(item.description) : '', + isDeprecated: item.isDeprecated || false, + deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined, + possibleTypes: item.possibleTypes || [], + } + } + + /** + * Prepare an input object item for rendering + */ + private async prepareInputObject(item: InputObjectT): Promise> { + return { + name: item.name, + slug: item.name.toLowerCase(), + description: item.description ? fastTextOnly(item.description) : '', + isDeprecated: item.isDeprecated || false, + deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined, + inputFields: await this.prepareFields(item.inputFields), + } + } + + /** + * Prepare a scalar item for rendering + */ + private async prepareScalar(item: ScalarT): Promise> { + return { + name: item.name, + slug: item.name.toLowerCase(), + description: item.description ? fastTextOnly(item.description) : '', + isDeprecated: item.isDeprecated || false, + deprecationReason: item.deprecationReason ? fastTextOnly(item.deprecationReason) : undefined, + } + } + + /** + * Prepare fields for rendering + */ + private async prepareFields(fields: FieldT[]): Promise>> { + return fields.map((field) => ({ + name: field.name, + type: field.type, + href: field.href, + description: field.description ? fastTextOnly(field.description) : '', + defaultValue: field.defaultValue, + isDeprecated: field.isDeprecated || false, + deprecationReason: field.deprecationReason + ? fastTextOnly(field.deprecationReason) + : undefined, + arguments: field.arguments + ? field.arguments.map((arg) => ({ + name: arg.name, + description: arg.description ? fastTextOnly(arg.description) : '', + defaultValue: arg.defaultValue, + type: { + name: arg.type.name, + href: arg.type.href, + }, + })) + : undefined, + })) + } +} diff --git a/src/article-api/transformers/index.ts b/src/article-api/transformers/index.ts index a759723782c5..1ee146a92089 100644 --- a/src/article-api/transformers/index.ts +++ b/src/article-api/transformers/index.ts @@ -1,5 +1,6 @@ import { TransformerRegistry } from './types' import { RestTransformer } from './rest-transformer' +import { GraphQLTransformer } from './graphql-transformer' /** * Global transformer registry @@ -10,6 +11,9 @@ export const transformerRegistry = new TransformerRegistry() // Register REST transformer transformerRegistry.register(new RestTransformer()) +// Register GraphQL transformer +transformerRegistry.register(new GraphQLTransformer()) + // Future transformers can be registered here: // transformerRegistry.register(new WebhooksTransformer()) // transformerRegistry.register(new GitHubAppsTransformer()) diff --git a/src/code-scanning/README.md b/src/code-scanning/README.md deleted file mode 100644 index 2dbadbf1306d..000000000000 --- a/src/code-scanning/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Code scanning query tables - -This nascent pipeline creates autogenerated documentation docs.github.com from the query suites included with the [CodeQL bundle](https://github.com/github/codeql-action/releases). - -The pipeline is used to generate Markdown tables that are stored in reusable files and used in article pages on the docs.github.com site. - -## How does it work - -```mermaid ---- -title: "Process for generating Code scanning query tables" ---- -flowchart TB - -accDescr: A flow chart describing how the automation generates documentation for code scanning queries. - - start([Start]) --> checkout[Checkout the codeql repository] - start --- download[Download the CodeQL CLI] - checkout --- query-suites[fa:fa-file Query suites *.qls] - query-suites ---> generate[Generate tables:src/code-scanning/scripts/generate-code-scanning-query-lists.ts] - download --- codeql-cli[CodeQL CLI: codeql.exe] - codeql-cli --> generate - generate --- markdown[fa:fa-file Reusable files *.md] - markdown --> pr[Generate a PR overwriting:data/reusables/code-scanning/codeql-query-tables/*.md] - pr --> finish([End]) - -%% Define styles - classDef start fill:#1AAC9D, color:white - classDef action fill:#6557F6, color:white - classDef finish fill:#F8C324, color:white - classDef file fill:#ddd - -%% Assign styles - class start start; - class finish finish; - class checkout,download,generate,pr action; - class markdown,query-suites,codeql-cli file; -``` - -A [workflow](.github/workflows/generate-code-scanning-query-lists.yml) is used to trigger the automation of the code scanning query tables documentation. The workflow is manually triggered by a member of the GitHub Docs team approximately every two weeks to align to releases of the CodeQL CLI. The workflow takes an input parameter that specifies the branch to pull the source files from in the semmle-code repo. If the branch input is omitted, the workflow will default to the `main` branch. - -The workflow runs the `npm run generate-code-scanning-query-list` script, which generates Markdown files under `data/reusables/code-scanning/codeql-query-tables`. - -The workflow automatically creates a new pull request with the changes and the label `codeql-query-tables`. - -## Local development - -To run the pipeline locally, see the comments in the [script](scripts/generate-code-scanning-query-list.ts). - -## Content team - -The content writers can use the reusables in any content article. They have no need to make any changes to the script unless additional built-in query suites are added. - -## How to get help - -### For workflow and script problems - -Slack: `#docs-engineering` -Repo: `github/docs-engineering` - -### For CodeQL repository and CLI problems - -Slack: `#code-scanning-internal-dx` -Repo: `github/code-scanning-internal-dx-team` diff --git a/src/code-scanning/scripts/tsconfig.json b/src/code-scanning/scripts/tsconfig.json deleted file mode 100644 index 11ddf7df116a..000000000000 --- a/src/code-scanning/scripts/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../../tsconfig.json", - "include": ["generate-code-scanning-query-list.ts"], - "exclude": ["node_modules"] -} diff --git a/src/codeql-queries/README.md b/src/codeql-queries/README.md new file mode 100644 index 000000000000..5bf9bd0f092b --- /dev/null +++ b/src/codeql-queries/README.md @@ -0,0 +1,88 @@ +# CodeQL query tables + +This pipeline creates autogenerated documentation for docs.github.com from the query suites included with the [CodeQL bundle](https://github.com/github/codeql-action/releases). + +The pipeline generates Markdown tables that are stored in reusable files and used in article pages on the docs.github.com site. + +There are two types of query table generators: + +1. **Security queries** (`generate-code-scanning-query-list.ts`) - generates tables for security-related queries from the `code-scanning` and `security-extended` suites, including CWE information and autofix support. +2. **Code quality queries** (`generate-code-quality-query-list.ts`) - generates tables for code quality queries from the `code-quality` suite, showing query categories (reliability, maintainability). + +## How does it work + +```mermaid +--- +title: "Process for generating CodeQL query tables" +--- +flowchart TB + +accDescr: A flow chart describing how the automation generates documentation for CodeQL queries. + + start([Start]) --> checkout[Checkout the codeql repository] + start --> download[Download the CodeQL CLI] + + checkout --> security-suites[fa:fa-file Security query suites *.qls] + checkout --> quality-suites[fa:fa-file Quality query suites *.qls] + + security-suites --> generate-security[Generate security tables] + quality-suites --> generate-quality[Generate quality tables] + + download --> codeql-cli[CodeQL CLI: codeql.exe] + codeql-cli --> generate-security + codeql-cli --> generate-quality + + generate-security --> security-markdown[fa:fa-file Security reusables *.md] + generate-quality --> quality-markdown[fa:fa-file Quality reusables *.md] + + security-markdown --> merge[Merge artifacts] + quality-markdown --> merge + + merge --> pr[Generate a PR with both:
data/reusables/code-scanning/codeql-query-tables/*.md
data/reusables/code-quality/codeql-query-tables/*.md] + pr --> finish([End]) + +%% Define styles + classDef start fill:#1AAC9D, color:white + classDef action fill:#6557F6, color:white + classDef finish fill:#F8C324, color:white + classDef file fill:#ddd + +%% Assign styles + class start start; + class finish finish; + class checkout,download,generate-security,generate-quality,merge,pr action; + class security-markdown,quality-markdown,security-suites,quality-suites,codeql-cli file; +``` + +A [workflow](../../.github/workflows/generate-codeql-query-lists.yml) is used to trigger the automation of the CodeQL query tables documentation. The workflow is manually triggered by a member of the GitHub Docs team approximately every two weeks to align to releases of the CodeQL CLI. The workflow takes an input parameter that specifies the branch to pull the source files from in the codeql repo. If the branch input is omitted, the workflow will default to the `main` branch. + +The workflow runs two scripts in parallel: +- `npm run generate-code-scanning-query-list` - generates security query tables under `data/reusables/code-scanning/codeql-query-tables/` +- `npm run generate-code-quality-query-list` - generates code quality query tables under `data/reusables/code-quality/codeql-query-tables/` + +The workflow automatically creates a new pull request with changes from both scripts and the label `codeql-query-tables`. + +## Local development + +To run the pipeline locally, see the comments in the scripts: +- Security queries: [generate-code-scanning-query-list.ts](scripts/generate-code-scanning-query-list.ts) +- Code quality queries: [generate-code-quality-query-list.ts](scripts/generate-code-quality-query-list.ts) + +## Content team + +The content writers can use the reusables in any content article. They have no need to make any changes to the scripts unless additional built-in query suites are added. + +For security queries, reusables are stored in `data/reusables/code-scanning/codeql-query-tables/`. +For code quality queries, reusables are stored in `data/reusables/code-quality/codeql-query-tables/`. + +## How to get help + +### For workflow and script problems + +Slack: `#docs-engineering` +Repo: `github/docs-engineering` + +### For CodeQL repository and CLI problems + +Slack: `#code-scanning-engine-quality` +Repo: `github/code-scanning-engine-quality-team` diff --git a/src/codeql-queries/scripts/generate-code-quality-query-list.ts b/src/codeql-queries/scripts/generate-code-quality-query-list.ts new file mode 100644 index 000000000000..a0348201000a --- /dev/null +++ b/src/codeql-queries/scripts/generate-code-quality-query-list.ts @@ -0,0 +1,291 @@ +/** + * This script generates a block of Markdown that can be saved as a reusable. + * The reusable lists all the code quality queries for one programming language, with categories, as a Markdown table. + * + * To be able to execute this script, you need to have the CodeQL CLI installed. + * To do that, you need two things: + * + * 1. The directory where the github/codeql repo is cloned + * 2. The path to the executable `codeql` file. + * + * The directory where the github/codeql repo is cloned is needed because + * that's how it looks up files. You can set it up like this: + * + * cd /tmp + * git clone git@github.com:github/codeql.git + * cd codeql + * pwd + * + * To install the codeql executable, use `gh` like this: + * + * gh extension install github/gh-codeql + * gh codeql set-channel nightly + * gh codeql version + * + * Note that when you run the `gh codeql version` command, it will tell you + * where the executable is installed. For example: + * + * /Users/peterbe/.local/share/gh/extensions/gh-codeql/dist/nightly/codeql-bundle-20231204/codeql + * + * If you've git cloned github/codeql in /tmp/ now you can execute this script. + * For example, to generate the Markdown + * for Python: + * + * npm run generate-code-quality-query-list -- \ + * --codeql-path ~/.local/share/gh/extensions/gh-codeql/dist/nightly/codeql-bundle-20231204/codeql \ + * --codeql-dir /tmp/codeql python | tee /tmp/python.md + * less /tmp/python.md + */ + +import fs from 'fs' +import { execFileSync } from 'child_process' + +import chalk from 'chalk' +import { program } from 'commander' + +program + .description('Generate a reusable Markdown for code quality queries by language') + .option('--verbose', 'Verbose outputs') + .option('--codeql-path ', 'path to the codeql executable', 'codeql') + .option('--codeql-dir ', 'path to the codeql executable', '.codeql/') + .option('-o, --output-file ', 'output file path (default: stdout)', 'stdout') + .argument('', 'for example java') + .parse(process.argv) + +type Options = { + codeqlPath: string + codeqlDir: string + outputFile: string + verbose: boolean +} + +type QueryMetadata = { + id?: string + name?: string + tags?: string + severity?: string + problem?: { + severity?: string + } +} + +type Query = { + name: string + url: string + categories: string[] + severity: string +} + +type QueryExtended = Query & { + primaryCategory: string +} + +const opts = program.opts() +main( + { + codeqlPath: opts.codeqlPath, + codeqlDir: opts.codeqlDir, + outputFile: opts.outputFile, + verbose: Boolean(opts.verbose), + }, + program.args[0], +) + +async function main(options: Options, language: string) { + if (options.verbose && options.outputFile === 'stdout') { + console.warn(chalk.yellow('Verbose mode is on but output is going to stdout')) + } + + if (!testCodeQLPath(options)) { + process.exit(1) + } + + const queries: { + [id: string]: Query + } = {} + + const languagePack = `${language}-code-quality.qls` + if (options.verbose) console.log(chalk.dim(`Searching for queries in ${languagePack}`)) + const res = execFileSync( + options.codeqlPath, + ['resolve', 'queries', `--search-path=${options.codeqlDir}`, languagePack], + { + encoding: 'utf-8', + }, + ) + for (const line of res.split('\n')) { + if (line.trim()) { + if (options.verbose) console.log('found', line) + const metadata = getMetadata(options, line) + const { id, name, tags, severity } = metadata + if (id && name) { + const categories = getCategories(tags || '') + const url = getDocsLink(language, id) + + // Only include queries that have categories + if (categories.length) { + queries[id] = { url, name, categories, severity: severity || 'N/A' } + } else { + if (options.verbose) { + console.log(chalk.dim(`Skipping ${id} because it has no categories`)) + } + } + } + } + } + + function decorate(query: Query): QueryExtended { + // Determine primary category for sorting + // Prefer 'maintainability' over 'reliability' + const primaryCategory = query.categories.includes('maintainability') + ? 'maintainability' + : query.categories.includes('reliability') + ? 'reliability' + : query.categories[0] || '' + + return { + ...query, + primaryCategory, + } + } + + const entries = Object.values(queries).map(decorate) + + // Sort by primary category (maintainability first), then alphabetically by name + entries.sort((a, b) => { + if (a.primaryCategory === 'maintainability' && b.primaryCategory !== 'maintainability') + return -1 + else if (a.primaryCategory !== 'maintainability' && b.primaryCategory === 'maintainability') + return 1 + + if (a.primaryCategory === 'reliability' && b.primaryCategory !== 'reliability') return -1 + else if (a.primaryCategory !== 'reliability' && b.primaryCategory === 'reliability') return 1 + + return a.name.localeCompare(b.name) + }) + + printQueries(options, entries) +} + +function printQueries(options: Options, queries: QueryExtended[]) { + const markdown: string[] = [] + markdown.push('{% rowheaders %}') + markdown.push('') // blank line + const header = ['Query name', 'Category', 'Severity'] + markdown.push(`| ${header.join(' | ')} |`) + markdown.push(`| ${header.map(() => '---').join(' | ')} |`) + + for (const query of queries) { + const markdownLink = `[${query.name}](${query.url})` + // Capitalize first letter of category for display + const categoryDisplay = query.categories + .map((cat) => cat.charAt(0).toUpperCase() + cat.slice(1)) + .join(', ') + // Capitalize first letter of severity for display + const severityDisplay = query.severity.charAt(0).toUpperCase() + query.severity.slice(1) + const row = [markdownLink, categoryDisplay, severityDisplay] + markdown.push(`| ${row.join(' | ')} |`) + } + markdown.push('') // blank line + markdown.push('{% endrowheaders %}') + markdown.push('') // always end with a blank line + + if (options.outputFile === 'stdout') { + console.log(markdown.join('\n')) + } else { + fs.writeFileSync(options.outputFile, markdown.join('\n'), 'utf-8') + } +} + +function getMetadata(options: Options, queryFile: string): QueryMetadata { + const metadataJson = execFileSync(options.codeqlPath, ['resolve', 'metadata', queryFile], { + encoding: 'utf-8', + }) + const parsed = JSON.parse(metadataJson) + + // Extract severity from various possible locations in the metadata + // CodeQL metadata can have @problem.severity in the query file, which may be + // represented in different ways in the JSON output from `codeql resolve metadata` + const severity = + parsed.problem?.severity || // Nested: { problem: { severity: "error" } } + parsed['@problem']?.severity || // Nested with @: { "@problem": { severity: "error" } } + parsed['@problem.severity'] || // Direct key: { "@problem.severity": "error" } + parsed['problem.severity'] || // Direct key without @: { "problem.severity": "error" } + parsed.severity || // Simple: { severity: "error" } + parsed['@severity'] // With @: { "@severity": "error" } + + if (options.verbose) { + // On first query only, show all available keys to help debug + if (!getMetadata.shownKeys) { + console.log(chalk.yellow('Available metadata keys:'), Object.keys(parsed)) + if (parsed.problem) { + console.log(chalk.yellow('Available problem keys:'), Object.keys(parsed.problem)) + } + if (parsed['@problem']) { + console.log(chalk.yellow('Available @problem keys:'), Object.keys(parsed['@problem'])) + } + getMetadata.shownKeys = true + } + if (severity) { + console.log(chalk.dim(`Query ${parsed.id} has severity: ${severity}`)) + } else { + console.log(chalk.red(`Query ${parsed.id} has NO severity found`)) + console.log(chalk.red('Available keys for this query:'), Object.keys(parsed)) + } + } + + return { + ...parsed, + severity, + } +} + +// Add a property to track if we've shown keys +getMetadata.shownKeys = false + +/** + * + * @param language 'cpp' + * @param queryId 'external-entity-expansion' + * @returns https://codeql.github.com/codeql-query-help/cpp/cpp-external-entity-expansion/ + */ +function getDocsLink(language: string, queryId: string) { + return `https://codeql.github.com/codeql-query-help/${language}/${queryId.replaceAll('/', '-')}/` +} + +/** + * + * @param tags 'maintainability readability reliability external/cwe/cwe-1078 external/cwe/cwe-670 security' + * @returns ['maintainability', 'reliability'] + */ +function getCategories(tags: string) { + const categories: string[] = [] + for (const tag of tags.split(/\s+/g)) { + if (tag === 'maintainability' || tag === 'reliability') { + categories.push(tag) + } + } + return categories +} + +function testCodeQLPath(options: Options) { + try { + const output = execFileSync(options.codeqlPath, ['--version'], { encoding: 'utf-8' }) + if (options.verbose) { + const matched = output.match(/CodeQL command-line toolchain release ([\d.+]+)/) + if (matched) { + console.log('codeql version', chalk.green(matched[0])) + return true + } + } + return true + } catch (error) { + console.error('Could not find codeql executable at', options.codeqlPath) + if (options.verbose) { + throw error + } else { + console.log(chalk.yellow(`${options.codeqlPath} --version`), 'failed') + return false + } + } +} diff --git a/src/code-scanning/scripts/generate-code-scanning-query-list.ts b/src/codeql-queries/scripts/generate-code-scanning-query-list.ts similarity index 98% rename from src/code-scanning/scripts/generate-code-scanning-query-list.ts rename to src/codeql-queries/scripts/generate-code-scanning-query-list.ts index d02e6e311439..8dfa75149b2c 100644 --- a/src/code-scanning/scripts/generate-code-scanning-query-list.ts +++ b/src/codeql-queries/scripts/generate-code-scanning-query-list.ts @@ -53,7 +53,8 @@ import chalk from 'chalk' import { program } from 'commander' // We don't want to introduce a global dependency on @github/cocofix, so we install it by hand // as described above and suppress the import warning. -import { getSupportedQueries } from '@github/cocofix/dist/querySuites' /* eslint-disable-line import/no-extraneous-dependencies, import/no-unresolved */ +// eslint-disable-next-line import/no-unresolved -- @github/cocofix is installed manually +import { getSupportedQueries } from '@github/cocofix/dist/querySuites' import type { Language } from 'codeql-ts' program diff --git a/src/codeql-queries/scripts/tsconfig.json b/src/codeql-queries/scripts/tsconfig.json new file mode 100644 index 000000000000..ebbc6a4ae1d3 --- /dev/null +++ b/src/codeql-queries/scripts/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["generate-code-scanning-query-list.ts", "generate-code-quality-query-list.ts"], + "exclude": ["node_modules"] +} diff --git a/src/content-render/scripts/resolve-liquid.ts b/src/content-render/scripts/resolve-liquid.ts new file mode 100644 index 000000000000..b76905842b4f --- /dev/null +++ b/src/content-render/scripts/resolve-liquid.ts @@ -0,0 +1,822 @@ +/* + * @purpose Writer tool + * @description Resolve and unresolve Liquid data references in content files + */ +// Usage: npm run resolve-liquid -- resolve --paths content/pull-requests/about.md +// Usage: npm run resolve-liquid -- restore --paths content/pull-requests/about.md + +import { Command } from 'commander' +import fs from 'fs' +import path from 'path' +import yaml from 'js-yaml' +import chalk from 'chalk' + +// Type definitions +interface ResolveOptions { + paths: string[] + verbose?: boolean + markers?: boolean + dryRun?: boolean + reusablesOnly?: boolean + variablesOnly?: boolean + recursive?: boolean +} + +interface LiquidReference { + original: string + type: 'reusable' | 'variable' + path: string + startIndex: number + endIndex: number +} + +// Constants +const ROOT = process.env.ROOT || '.' +const DATA_ROOT = path.resolve(path.join(ROOT, 'data')) +const REUSABLES_ROOT = path.join(DATA_ROOT, 'reusables') +const VARIABLES_ROOT = path.join(DATA_ROOT, 'variables') + +// Regex pattern to match resolved content blocks +const RESOLVED_PATTERN = + /(.+?)/gs + +/** + * Get the file path for a data reference + */ +function getDataFilePath(type: 'reusable' | 'variable', dataPath: string): string { + if (type === 'reusable') { + return path.join(REUSABLES_ROOT, `${dataPath.replace(/\./g, '/')}.md`) + } else { + const fileName = dataPath.split('.')[0] + return path.join(VARIABLES_ROOT, `${fileName}.yml`) + } +} + +const program = new Command() + +program + .name('resolve-liquid') + .description('Tools to resolve and unresolve Liquid data references in content files') + +program + .command('resolve') + .description('Resolve {% data reusables %} and {% data variables %} statements to their content') + .option('--paths ', 'Content file paths to process', []) + .option('-v, --verbose', 'Verbose output', false) + .option('--no-markers', 'Skip HTML comment markers (output cannot be restored to Liquid)', true) + .option('--reusables-only', 'Process only reusables (skip variables)', false) + .option('--variables-only', 'Process only variables (skip reusables)', false) + .option('-r, --recursive', 'Keep resolving until no references remain (max 10 iterations)', false) + .action((options: ResolveOptions) => resolveReferences(options)) + +program + .command('restore') + .description('Restore original Liquid statements from HTML comment markers') + .option('--paths ', 'Content file paths to process', []) + .option('-v, --verbose', 'Verbose output', false) + .option('--reusables-only', 'Process only reusables (skip variables)', false) + .option('--variables-only', 'Process only variables (skip reusables)', false) + .action((options: ResolveOptions) => restoreReferences(options)) + +program.parse() + +/** + * Get allowed types based on command options + */ +function getAllowedTypes(options: ResolveOptions): Array<'reusable' | 'variable'> { + if (options.reusablesOnly && options.variablesOnly) { + console.log( + chalk.yellow( + 'Warning: Both --reusables-only and --variables-only specified. Processing both types.', + ), + ) + return ['reusable', 'variable'] + } + + if (options.reusablesOnly) { + return ['reusable'] + } + + if (options.variablesOnly) { + return ['variable'] + } + + // Default: process both types + return ['reusable', 'variable'] +} + +/** + * Resolve Liquid data references in content files + */ +async function resolveReferences(options: ResolveOptions): Promise { + const { paths, verbose, markers, recursive } = options + // markers will be true by default, false when --no-markers is used + const withMarkers = markers !== false + const allowedTypes = getAllowedTypes(options) + const maxIterations = 10 // Safety limit for recursive resolution + + if (paths.length === 0) { + console.error(chalk.red('Error: No paths provided. Use --paths option.')) + process.exit(1) + } + + for (const filePath of paths) { + try { + let iteration = 0 + let hasRemainingRefs = true + + while (hasRemainingRefs && iteration < maxIterations) { + iteration++ + + if (verbose && recursive && iteration > 1) { + console.log(chalk.blue(`Processing (iteration ${iteration}): ${filePath}`)) + } else if (verbose) { + console.log(chalk.blue(`Processing: ${filePath}`)) + if (allowedTypes.length < 2) { + console.log(chalk.dim(` Only processing: ${allowedTypes.join(', ')}`)) + } + } + + if (!fs.existsSync(filePath)) { + console.error(chalk.red(`Error: File not found: ${filePath}`)) + break + } + + const content = fs.readFileSync(filePath, 'utf-8') + const resolvedContent = await resolveFileContent( + content, + filePath, + verbose, + withMarkers, + allowedTypes, + ) + + if (resolvedContent !== content) { + fs.writeFileSync(filePath, resolvedContent, 'utf-8') + if (iteration === 1 || !recursive) { + console.log(chalk.green(`✓ Resolved references in: ${filePath}`)) + } + } else { + if (verbose && iteration === 1) { + console.log(chalk.gray(` No references found in: ${filePath}`)) + } + } + + // Check for remaining references + const remainingRefs = findLiquidReferences(resolvedContent, allowedTypes) + hasRemainingRefs = remainingRefs.length > 0 + + if (!recursive) { + // Non-recursive mode: show remaining references and break + if (hasRemainingRefs) { + console.log( + chalk.yellow( + `👉 FYI: ${remainingRefs.length} Liquid reference(s) remain in ${filePath}`, + ), + ) + console.log( + chalk.yellow( + ' These come from reusables/variables that contain references to other reusables/variables', + ), + ) + console.log( + chalk.yellow(' Run the resolve command again to resolve them, or use --recursive'), + ) + if (verbose) { + for (const ref of remainingRefs) { + console.log(chalk.dim(` ${ref.original}`)) + } + } else { + console.log(chalk.dim(' Use --verbose to see the specific references')) + } + } + break + } + + if (hasRemainingRefs && iteration >= maxIterations) { + console.log( + chalk.yellow(`⚠️ Reached maximum iterations (${maxIterations}) for ${filePath}`), + ) + console.log( + chalk.yellow( + ` ${remainingRefs.length} reference(s) still remain - there may be circular references`, + ), + ) + if (verbose) { + for (const ref of remainingRefs) { + console.log(chalk.dim(` ${ref.original}`)) + } + } + } else if (!hasRemainingRefs && iteration > 1) { + console.log( + chalk.green( + `✓ Fully resolved all references in: ${filePath} (${iteration} iterations)`, + ), + ) + } + } + } catch (error: any) { + console.error(chalk.red(`Error processing ${filePath}: ${error.message}`)) + } + } +} + +/** + * Restore content by restoring original Liquid statements from HTML comments + */ +async function restoreReferences(options: ResolveOptions): Promise { + const { paths, verbose } = options + const allowedTypes = getAllowedTypes(options) + + if (paths.length === 0) { + console.error(chalk.red('Error: No paths provided. Use --paths option.')) + process.exit(1) + } + + for (const filePath of paths) { + try { + if (verbose) { + console.log(chalk.blue(`Restoring: ${filePath}`)) + if (allowedTypes.length < 2) { + console.log(chalk.dim(` Only processing: ${allowedTypes.join(', ')}`)) + } + } + + if (!fs.existsSync(filePath)) { + console.error(chalk.red(`Error: File not found: ${filePath}`)) + continue + } + + const content = fs.readFileSync(filePath, 'utf-8') + + // Check for content edits before restoring + const hasEdits = await detectContentEdits(content, verbose, allowedTypes) + if (hasEdits) { + console.log( + chalk.blue( + `ℹ️ Info: ${filePath} contains resolved references that will be preserved by updating data files`, + ), + ) + if (!verbose) { + console.log(chalk.dim(' Use --verbose to see details of the edits')) + } + + // Update data files with the edited content before restoring + const updatedDataFiles = updateDataFiles(filePath, verbose, false, allowedTypes) + + // Automatically restore any updated data files back to liquid tags + if (updatedDataFiles.length > 0) { + console.log(chalk.blue(' Restoring updated data files back to liquid tags...')) + for (const dataFile of updatedDataFiles) { + try { + const dataContent = fs.readFileSync(dataFile, 'utf-8') + const restoredDataContent = restoreFileContent(dataContent, verbose, allowedTypes) + if (restoredDataContent !== dataContent) { + fs.writeFileSync(dataFile, restoredDataContent, 'utf-8') + if (verbose) { + console.log(chalk.green(` Restored: ${dataFile}`)) + } + } + } catch (error) { + if (verbose) { + console.log(chalk.yellow(` Could not restore ${dataFile}: ${error}`)) + } + } + } + } + } + + // Always restore the main file content regardless of edits + const restoredContent = restoreFileContent(content, verbose, allowedTypes) + + if (restoredContent !== content) { + fs.writeFileSync(filePath, restoredContent, 'utf-8') + console.log(chalk.green(`✓ Restored references in: ${filePath}`)) + } else { + console.log(chalk.gray(`No resolved references found in: ${filePath}`)) + } + } catch (error: any) { + console.error(chalk.red(`Error restoring ${filePath}: ${error.message}`)) + } + } +} + +/** + * Resolve all Liquid data references in file content + */ +async function resolveFileContent( + content: string, + filePath: string, + verbose?: boolean, + withMarkers?: boolean, + allowedTypes?: Array<'reusable' | 'variable'>, +): Promise { + const references = findLiquidReferences(content, allowedTypes) + + if (references.length === 0) { + return content + } + + let resolvedContent = content + let offset = 0 + + for (const ref of references) { + try { + const resolvedValue = await resolveLiquidReference(ref, verbose) + + if (resolvedValue !== null) { + const originalText = ref.original + let replacement: string + + if (withMarkers) { + const commentStart = `` + const commentEnd = `` + replacement = `${commentStart}${resolvedValue}${commentEnd}` + } else { + replacement = resolvedValue + } + + const startPos = ref.startIndex + offset + const endPos = ref.endIndex + offset + + resolvedContent = + resolvedContent.substring(0, startPos) + replacement + resolvedContent.substring(endPos) + + offset += replacement.length - originalText.length + + if (verbose) { + console.log(chalk.green(` Resolved: ${ref.type}s.${ref.path}`)) + } + } else { + if (verbose) { + console.log(chalk.yellow(` Warning: Could not resolve ${ref.type}s.${ref.path}`)) + } + } + } catch (error: any) { + if (verbose) { + console.log(chalk.red(` Error resolving ${ref.type}s.${ref.path}: ${error.message}`)) + } + } + } + + // Note: Remaining reference detection is now handled in resolveReferences function for recursive mode + + return resolvedContent +} + +/** + * Detect if resolved content has been edited by comparing with original data + */ +async function detectContentEdits( + content: string, + verbose?: boolean, + allowedTypes?: Array<'reusable' | 'variable'>, +): Promise { + let hasEdits = false + + let match + while ((match = RESOLVED_PATTERN.exec(content)) !== null) { + const [, type, dataPath, resolvedContent] = match + const refType = type as 'reusable' | 'variable' + + // Only check if this type is allowed + if (!allowedTypes || allowedTypes.includes(refType)) { + try { + // Load the original content from data files + const originalContent = loadDataValue(refType, dataPath.trim()) + + if (originalContent !== null) { + // Compare against the original content directly, not re-resolved + // This avoids nested resolution issues that cause false positives + const currentContent = resolvedContent.trim() + + if (currentContent !== originalContent.trim()) { + hasEdits = true + if (verbose) { + console.log(chalk.yellow(` Content has been edited: ${type}s.${dataPath}`)) + console.log( + chalk.dim(' Original:'), + originalContent.substring(0, 50) + (originalContent.length > 50 ? '...' : ''), + ) + console.log( + chalk.dim(' Current: '), + currentContent.substring(0, 50) + (currentContent.length > 50 ? '...' : ''), + ) + } + } + } + } catch (error) { + if (verbose) { + console.log(chalk.yellow(` Could not verify content for ${type}s.${dataPath}: ${error}`)) + } + } + } + } + + return hasEdits +} + +/** + * Load data value from file system (helper for edit detection) + */ +function loadDataValue(type: 'reusable' | 'variable', dataPath: string): string | null { + try { + const targetPath = getDataFilePath(type, dataPath) + + if (!fs.existsSync(targetPath)) { + return null + } + + if (type === 'reusable') { + const content = fs.readFileSync(targetPath, 'utf8') + // Remove any frontmatter if present (same as resolveReusable) + const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, '') + return contentWithoutFrontmatter.trim() + } else { + const yamlContent = fs.readFileSync(targetPath, 'utf8') + const data = yaml.load(yamlContent) as any + + // Navigate to the nested property + const pathParts = dataPath.split('.') + let current = data + for (let i = 1; i < pathParts.length; i++) { + if (current && typeof current === 'object' && pathParts[i] in current) { + current = current[pathParts[i]] + } else { + return null + } + } + + return typeof current === 'string' ? current.trim() : String(current).trim() + } + } catch { + // Silently return null for any errors + } + return null +} + +/** + * Restore content by restoring original Liquid statements + */ +function restoreFileContent( + content: string, + verbose?: boolean, + allowedTypes?: Array<'reusable' | 'variable'>, +): string { + return content.replace(RESOLVED_PATTERN, (match, type, dataPath) => { + const refType = type as 'reusable' | 'variable' + + // Only restore if this type is allowed + if (!allowedTypes || allowedTypes.includes(refType)) { + const originalLiquid = `{% data ${type}s.${dataPath} %}` + + if (verbose) { + console.log(chalk.green(` Restored: ${type}s.${dataPath}`)) + } + + return originalLiquid + } + + // Return unchanged if type is not allowed + return match + }) +} + +/** + * Update data files with content from resolved blocks + * Returns array of file paths that were updated + */ +function updateDataFiles( + filePath: string, + verbose?: boolean, + dryRun?: boolean, + allowedTypes?: Array<'reusable' | 'variable'>, +): string[] { + const content = fs.readFileSync(filePath, 'utf8') + const updates = extractDataUpdates(content, allowedTypes) + + if (updates.length === 0) { + if (verbose) { + console.log(chalk.yellow(' No content changes found')) + } + return [] + } + + // Group updates by file path + const updatesByFile = new Map() + for (const update of updates) { + const key = `${update.type}:${update.path}` + if (!updatesByFile.has(key)) { + updatesByFile.set(key, []) + } + updatesByFile.get(key)!.push(update.newContent) + } + + const updatedFiles: string[] = [] + + // Apply updates to each data file + for (const [key, contents] of updatesByFile) { + const [type, dataPath] = key.split(':') + const targetFilePath = applyDataUpdates( + type as 'reusable' | 'variable', + dataPath, + contents, + verbose, + dryRun, + ) + if (targetFilePath) { + updatedFiles.push(targetFilePath) + } + } + + return updatedFiles +} + +/** + * Extract data updates from resolved content blocks + */ +function extractDataUpdates( + content: string, + allowedTypes?: Array<'reusable' | 'variable'>, +): Array<{ type: 'reusable' | 'variable'; path: string; newContent: string }> { + const updates: Array<{ type: 'reusable' | 'variable'; path: string; newContent: string }> = [] + + let match + while ((match = RESOLVED_PATTERN.exec(content)) !== null) { + const [, type, dataPath, resolvedContent] = match + const refType = type as 'reusable' | 'variable' + + // Only include if this type is allowed + if (!allowedTypes || allowedTypes.includes(refType)) { + // Check if this content was actually changed before including it + try { + const originalContent = loadDataValue(refType, dataPath.trim()) + if (originalContent !== null && resolvedContent.trim() !== originalContent.trim()) { + // Only add to updates if content was actually changed + updates.push({ + type: refType, + path: dataPath.trim(), + newContent: resolvedContent.trim(), + }) + } + } catch { + // If we can't verify, assume it was changed to be safe + updates.push({ + type: refType, + path: dataPath.trim(), + newContent: resolvedContent.trim(), + }) + } + } + } + + return updates +} + +/** + * Apply updates to a specific data file + * Returns the file path if file was updated, null otherwise + */ +function applyDataUpdates( + type: 'reusable' | 'variable', + dataPath: string, + contents: string[], + verbose?: boolean, + dryRun?: boolean, +): string | null { + const targetPath = getDataFilePath(type, dataPath) + + // Check if file exists + if (!fs.existsSync(targetPath)) { + if (verbose) { + console.log(chalk.red(` Error: Data file not found: ${targetPath}`)) + } + return null + } + + if (dryRun) { + if (verbose) { + console.log(chalk.blue(` Would update: ${targetPath}`)) + if (contents.length > 1) { + console.log( + chalk.yellow( + ` Warning: Multiple content blocks found for ${dataPath}, would use first one`, + ), + ) + } + console.log( + chalk.dim( + ` New content: ${contents[0].substring(0, 100)}${contents[0].length > 100 ? '...' : ''}`, + ), + ) + } else { + console.log(chalk.green(` Updated: ${targetPath}`)) + } + return targetPath // Return path even in dry run + } + + try { + if (type === 'reusable') { + // For reusables, replace entire file content + if (contents.length > 1) { + console.log( + chalk.yellow(` Warning: Multiple content blocks found for ${dataPath}, using first one`), + ) + } + + // Preserve original file's newline behavior + const originalContent = fs.readFileSync(targetPath, 'utf8') + const hasTrailingNewline = originalContent.endsWith('\n') + const newContent = + hasTrailingNewline && !contents[0].endsWith('\n') ? `${contents[0]}\n` : contents[0] + + fs.writeFileSync(targetPath, newContent) + if (verbose) { + console.log(chalk.green(` Updated: ${targetPath}`)) + } + } else { + // For variables, update YAML structure + const yamlContent = fs.readFileSync(targetPath, 'utf8') + const data = yaml.load(yamlContent) as any + + // Navigate to the nested property + const pathParts = dataPath.split('.') + const propertyPath = pathParts.slice(1) // Skip the file name + + let current = data + for (let i = 0; i < propertyPath.length - 1; i++) { + if (!current[propertyPath[i]]) { + current[propertyPath[i]] = {} + } + current = current[propertyPath[i]] + } + + // Update the final property + const finalKey = propertyPath[propertyPath.length - 1] + if (contents.length > 1) { + console.log( + chalk.yellow(` Warning: Multiple content blocks found for ${dataPath}, using first one`), + ) + } + current[finalKey] = contents[0] + + // Preserve original file's newline behavior for YAML + const hasTrailingNewline = yamlContent.endsWith('\n') + const yamlOutput = yaml.dump(data) + const finalYaml = + hasTrailingNewline && !yamlOutput.endsWith('\n') ? `${yamlOutput}\n` : yamlOutput + + // Write back to file + fs.writeFileSync(targetPath, finalYaml) + if (verbose) { + console.log(chalk.green(` Updated: ${targetPath}`)) + } + } + return targetPath + } catch (error: any) { + if (verbose) { + console.log(chalk.red(` Error updating ${targetPath}: ${error.message}`)) + } + return null + } +} + +/** + * Find all Liquid data references in content + */ +function findLiquidReferences( + content: string, + allowedTypes?: Array<'reusable' | 'variable'>, +): LiquidReference[] { + const references: LiquidReference[] = [] + const types = allowedTypes || ['reusable', 'variable'] + + // Pattern to match {% data reusables.path %} and {% data variables.path %} + const liquidPattern = /{%\s*data\s+(reusables|variables)\.([^%]+)\s*%}/g + + let match + while ((match = liquidPattern.exec(content)) !== null) { + const [original, type, dataPath] = match + const refType = type.slice(0, -1) as 'reusable' | 'variable' // Remove 's' from end + + // Only include if this type is allowed + if (types.includes(refType)) { + references.push({ + original, + type: refType, + path: dataPath.trim(), + startIndex: match.index, + endIndex: match.index + original.length, + }) + } + } + + return references +} + +/** + * Resolve a single Liquid data reference to its content + */ +async function resolveLiquidReference( + ref: LiquidReference, + verbose?: boolean, +): Promise { + try { + if (ref.type === 'reusable') { + return await resolveReusable(ref.path, verbose) + } else if (ref.type === 'variable') { + return await resolveVariable(ref.path, verbose) + } + } catch (error: any) { + if (verbose) { + console.log(chalk.red(` Error resolving ${ref.type}: ${error.message}`)) + } + } + + return null +} + +/** + * Resolve a reusable reference by reading the markdown file + */ +async function resolveReusable(reusablePath: string, verbose?: boolean): Promise { + const filePath = getDataFilePath('reusable', reusablePath) + + if (!fs.existsSync(filePath)) { + if (verbose) { + console.log(chalk.yellow(` Reusable not found: ${reusablePath}`)) + } + return null + } + + try { + const content = fs.readFileSync(filePath, 'utf-8') + // Remove any frontmatter if present + const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\s*/, '') + return contentWithoutFrontmatter.trim() + } catch (error: any) { + if (verbose) { + console.log(chalk.yellow(` Error reading reusable ${reusablePath}: ${error.message}`)) + } + return null + } +} + +/** + * Resolve a variable reference by reading from YAML files + */ +async function resolveVariable(variablePath: string, verbose?: boolean): Promise { + const pathParts = variablePath.split('.') + + if (pathParts.length < 2) { + if (verbose) { + console.log(chalk.yellow(` Invalid variable path: ${variablePath}`)) + } + return null + } + + const filePath = getDataFilePath('variable', variablePath) + + if (!fs.existsSync(filePath)) { + if (verbose) { + console.log(chalk.yellow(` Variable file not found: ${filePath}`)) + } + return null + } + + try { + const yamlContent = fs.readFileSync(filePath, 'utf-8') + const data = yaml.load(yamlContent) as Record + + // Navigate through the key path to find the value + const [, ...keyPath] = pathParts // Skip filename, get remaining path + let value: any = data + for (const key of keyPath) { + if (value && typeof value === 'object' && key in value) { + value = value[key] + } else { + if (verbose) { + console.log(chalk.yellow(` Variable key not found: ${variablePath}`)) + } + return null + } + } + + // Convert value to string + if (typeof value === 'string') { + return value + } else if (value !== null && value !== undefined) { + return String(value) + } else { + if (verbose) { + console.log(chalk.yellow(` Variable value is null/undefined: ${variablePath}`)) + } + return null + } + } catch (error: any) { + if (verbose) { + console.log(chalk.yellow(` Error parsing variable ${variablePath}: ${error.message}`)) + } + return null + } +} diff --git a/src/content-render/tests/resolve-liquid.ts b/src/content-render/tests/resolve-liquid.ts new file mode 100644 index 000000000000..70e23a627ad6 --- /dev/null +++ b/src/content-render/tests/resolve-liquid.ts @@ -0,0 +1,98 @@ +import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest' +import fs from 'fs/promises' +import path from 'path' +import { execSync } from 'child_process' + +const rootDir = path.join(__dirname, '../../..') +const testContentDir = path.join(rootDir, 'content/test-integration') + +describe('resolve-liquid script integration tests', () => { + vi.setConfig({ testTimeout: 60 * 1000 }) + + beforeEach(async () => { + // Create test directory + await fs.mkdir(testContentDir, { recursive: true }) + }) + + afterEach(async () => { + // Clean up test files + await fs.rm(testContentDir, { recursive: true, force: true }) + }) + + // Helper function to run script commands + async function runResolveScript(args: string): Promise<{ output: string; exitCode: number }> { + let output = '' + let exitCode = 0 + + try { + output = execSync(`tsx src/content-render/scripts/resolve-liquid.ts ${args}`, { + encoding: 'utf8', + cwd: rootDir, + stdio: 'pipe', + timeout: 30000, + }) + } catch (error: any) { + output = error.stdout + error.stderr + exitCode = error.status || 1 + } + + return { output, exitCode } + } + + test('resolve command should complete successfully with basic content', async () => { + // Create a test file with liquid reference + const testFile = path.join(testContentDir, 'basic-test.md') + const testContent = `--- +title: Test +--- + +This uses {% data variables.product.prodname_dotcom %} in content. +` + + await fs.writeFile(testFile, testContent) + + const { output, exitCode } = await runResolveScript(`resolve --paths "${testFile}"`) + + // Should complete without error + expect(exitCode, `Script failed with output: ${output}`).toBe(0) + expect(output.length).toBeGreaterThan(0) + + // Check that the file was modified + const resolvedContent = await fs.readFile(testFile, 'utf8') + expect(resolvedContent).not.toBe(testContent) + expect(resolvedContent).toContain('GitHub') // Should resolve to actual fixture value + }) + + test('restore command should complete successfully', async () => { + const testFile = path.join(testContentDir, 'restore-test.md') + const originalContent = `--- + title: Test + --- + +This uses {% data variables.product.prodname_dotcom %} in content. +` + + await fs.writeFile(testFile, originalContent) + + // First resolve + await runResolveScript(`resolve --paths "${testFile}"`) + + // Then restore + const { output, exitCode } = await runResolveScript(`restore --paths "${testFile}"`) + + expect(exitCode, `Restore script failed with output: ${output}`).toBe(0) + expect(output.length).toBeGreaterThan(0) + + // Should be back to original liquid tags + const restoredContent = await fs.readFile(testFile, 'utf8') + expect(restoredContent).toContain('{% data variables.product.prodname_dotcom %}') + expect(restoredContent).not.toContain('GitHub') + }) + + test('help command should display usage information', async () => { + const { output, exitCode } = await runResolveScript('resolve --help') + + expect(exitCode, `Help command failed with output: ${output}`).toBe(0) + expect(output).toMatch(/resolve|usage|help|command/i) + }) +}) diff --git a/src/data-directory/README.md b/src/data-directory/README.md index e69de29bb2d1..77fc9dd37848 100644 --- a/src/data-directory/README.md +++ b/src/data-directory/README.md @@ -0,0 +1,81 @@ +# Data directory + +Purpose-built utilities, schemas, and workflows that power our Liquid `{% data %}` and `{% indented_data_reference %}` tags, reusable content, UI strings, and feature metadata. This subject focuses on how we read, validate, and serve files in `data/` across languages. + +## Purpose & scope +- Provide a consistent API (`getDataByLanguage`, `getDeepDataByLanguage`) to load `data/` files for Liquid rendering and server contexts. +- Enforce schemas for critical data (features, variables, learning tracks, release notes, tables, glossaries, code languages, CTAs). +- Ship CLI and CI helpers that keep `data/` clean (orphaned feature detection, deleted-feature PR guardrails). +- Exclude: content authoring guidance (see `content/`), page routing (see `src/app`/`src/frame`), and general linter rules (see `src/content-linter`). + +## Architecture & key assets +- `lib/get-data.ts`: translation-aware loader with memoized reads, forced-English exceptions, and UI data merging; used by Liquid tags and server contexts. +- `lib/data-directory.ts` + `lib/filename-to-key.ts`: generic walker that turns files into dotted-key objects with optional preprocessing. +- `lib/data-schemas/`: AJV schema registry that auto-discovers `data/tables/*.yml` schemas and registers other critical shapes (features, variables, learning tracks, release notes, glossaries, code languages, CTAs). +- Middleware: `middleware/data-tables.ts` caches table data into `req.context.tables` (English). +- Scripts: `scripts/find-orphaned-features/*` (detect/delete unused `data/features/*.yml`) and `scripts/deleted-features-pr-comment.ts` (warn on feature deletions in PRs). +- Tests: `tests/` cover schema validation, data loading, key normalization, and orphan detection fixtures. + +## Data loading contracts +- `lib/get-data.ts` + - `getDataByLanguage(dottedPath, langCode)`: Returns a single value (YAML/MD/variables/reusables/ui/glossaries/release-notes/product-examples). + - `getDeepDataByLanguage(dottedPath, langCode)`: Returns nested objects for an entire subtree (e.g., `tables`, `features`). + - Translation fallbacks: If a localized file is missing or unparsable, falls back to English. Certain files are forced-English (`ALWAYS_ENGLISH_YAML_FILES`, `ALWAYS_ENGLISH_MD_FILES`). + - Memoization: Caches reads except in `NODE_ENV=development` to simplify local debugging. +- `lib/data-directory.ts` + - Recursively walks a directory, filters by extensions (`.json`, `.md/.markdown`, `.yml`) and ignore patterns, and emits a dotted-key object using `filename-to-key`. + - Optional `preprocess` hook for content transformation (used in tests/prior scripts). + +## Schemas and validation +- Schema registry: `lib/data-schemas/index.ts` maps data paths to schema modules; auto-registers any `data/tables/*.yml` that has a matching `data-schemas/tables/{name}.ts`. +- Tests: `src/data-directory/tests/data-schemas.ts` loads schemas via AJV and asserts every registered file validates. +- Adding a schema: + 1. Create `src/data-directory/lib/data-schemas/.ts` (or `tables/.ts`). + 2. If non-table, add to `manualSchemas` in `data-schemas/index.ts`; table schemas are auto-detected. + 3. Run tests (see below). + +## Middleware +- `middleware/data-tables.ts` populates `req.context.tables` with `getDeepDataByLanguage('tables', 'en')`. Intended for server/Express contexts where table data is needed without per-request file IO. + +## Scripts & workflows +- `npm run find-orphaned-features -- --source-directory data/features --output orphans.json` + - Scans pages, reusables, variables (all languages) for `{% ifversion %}` feature references and reports unused `data/features/*.yml`. +- `npm run find-orphaned-features delete -- orphans.json --max 10` + - Deletes up to N orphaned feature files (English root) after manual review. +- `npm run deleted-features-pr-comment -- ` + - Generates Markdown warning if a PR removes or renames feature files; used in CI (requires `GITHUB_TOKEN`). + +## Testing +- All tests: `npm test -- src/data-directory/tests` +- Targeted: + - Schemas: `npm test -- src/data-directory/tests/data-schemas.ts` + - Orphans: `npm test -- src/data-directory/tests/orphaned-features.ts` + - Loader basics: `npm test -- src/data-directory/tests/get-data.ts` + +## Data conventions and consumers +- File locations: Everything under `data/` (English and localized mirrors). Reusables/variables/ui are read via dotted paths (`reusables.foo.bar`, `variables.product.prodname_ghe_server`, `ui.pages.home`). +- Markdown in data: Frontmatter is stripped by `gray-matter`; content is trimmed. +- Downstream consumers: + - Liquid tags: `content-render/liquid/data.ts`, `indented-data-reference.ts` + - Content linter: `content-linter/lib/linting-rules/liquid-data-tags.ts`, `frontmatter-intro-links.ts` + - Server: `app/lib/app-router-context.ts`, `app/lib/server-context-utils.ts` + - Metrics/tests: `content-render/tests`, `content-linter/tests/site-data-references.ts` +- Translation notes: + - Fallbacks ensure missing localized YAML/MD reads from English. + - Specific files are forced-English to avoid corrupt translations (see constants in `get-data.ts`). + +## Setup & usage tips +- Ensure `data/` exists relative to project root; schemas auto-scan `data/tables` at runtime. +- Set `DEBUG_JIT_DATA_READS=true` to log every on-disk read from the data loaders; useful alongside tests or local runs to trace which data files are touched. +- When adding a new data directory: + - Prefer YAML for structured data; add schema if shape matters to correctness. + - Add README under `data//` when introducing new contracts. + - Update `manualSchemas` if not a table. + +## Ownership & escalation +- Primary: Docs Engineering. +- Content changes: Docs Content (docs-content). + +## Current state & next steps +- Current state: KTLO; minimal changes expected. Update this README when touching data loaders, schemas, or scripts. +- Next steps: Keep the schema registry aligned with new data shapes and rerun `npm test -- src/data-directory/tests` when data contracts change. \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/index.md b/src/fixtures/fixtures/content/graphql/index.md new file mode 100644 index 000000000000..0f156df84b52 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/index.md @@ -0,0 +1,12 @@ +--- +title: GitHub GraphQL API documentation +intro: 'To create integrations, retrieve data, and automate your workflows, use the {% data variables.product.prodname_dotcom %} GraphQL API.' +shortTitle: GraphQL API +versions: + fpt: '*' + ghec: '*' + ghes: '*' +children: + - /overview + - /reference +--- \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/overview/breaking-changes.md b/src/fixtures/fixtures/content/graphql/overview/breaking-changes.md new file mode 100644 index 000000000000..a00972605cdd --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/overview/breaking-changes.md @@ -0,0 +1,20 @@ +--- +title: Breaking changes +intro: Learn about recent and upcoming breaking changes to the {% data variables.product.prodname_dotcom %} GraphQL API. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About breaking changes + +Breaking changes are any changes that might require action from our integrators. We divide these changes into two categories: + +* **Breaking:** Changes that will break existing queries to the GraphQL API. For example, removing a field would be a breaking change. +* **Dangerous:** Changes that won't break existing queries but could affect the runtime behavior of clients. Adding an enum value is an example of a dangerous change. + +We'll announce upcoming breaking changes at least three months before making changes to the GraphQL schema, to give integrators time to make the necessary adjustments. + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/overview/changelog.md b/src/fixtures/fixtures/content/graphql/overview/changelog.md new file mode 100644 index 000000000000..248d15f70c0f --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/overview/changelog.md @@ -0,0 +1,13 @@ +--- +title: Changelog +intro: The GraphQL schema changelog is a list of recent and upcoming changes to our GraphQL API schema. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +Breaking changes include changes that will break existing queries or could affect the runtime behavior of clients. For a list of breaking changes and when they will occur, see our breaking changes log. + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/overview/index.md b/src/fixtures/fixtures/content/graphql/overview/index.md new file mode 100644 index 000000000000..3bf6a4f19795 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/overview/index.md @@ -0,0 +1,15 @@ +--- +title: Overview +intro: Learn about the GraphQL API overview topics. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +children: + - /breaking-changes + - /changelog +--- + +## GraphQL API overview + +The GraphQL API provides a powerful way to query GitHub data. \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/enums.md b/src/fixtures/fixtures/content/graphql/reference/enums.md new file mode 100644 index 000000000000..f6a15cadaf07 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/enums.md @@ -0,0 +1,17 @@ +--- +title: Enums +intro: Enums represent possible sets of values for a field. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About enums + +Enums are special types that define a set of possible values. When a field has an enum type, it can only be set to one of the predefined values. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/index.md b/src/fixtures/fixtures/content/graphql/reference/index.md new file mode 100644 index 000000000000..b93b3e6db2cb --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/index.md @@ -0,0 +1,20 @@ +--- +title: Reference +intro: View reference documentation to learn about the data types available in the {% data variables.product.prodname_dotcom %} GraphQL API schema. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +children: + - /queries + - /mutations + - /objects + - /interfaces + - /enums + - /unions + - /input-objects + - /scalars +autogenerated: graphql +--- + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/input-objects.md b/src/fixtures/fixtures/content/graphql/reference/input-objects.md new file mode 100644 index 000000000000..aa1265a6f104 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/input-objects.md @@ -0,0 +1,17 @@ +--- +title: Input objects +intro: Input objects are special types that allow you to pass complex objects as arguments to queries and mutations. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About input objects + +Input objects are used when you need to pass a structured set of values as an argument to a field. They are similar to regular objects, but they are specifically designed to be used as input arguments. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/interfaces.md b/src/fixtures/fixtures/content/graphql/reference/interfaces.md new file mode 100644 index 000000000000..f93c982d7cb3 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/interfaces.md @@ -0,0 +1,17 @@ +--- +title: Interfaces +intro: Interfaces are abstract types that include a certain set of fields that other types must include if they implement the interface. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About interfaces + +Interfaces allow you to define a set of fields that multiple object types can implement. They help ensure consistency across your schema. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/mutations.md b/src/fixtures/fixtures/content/graphql/reference/mutations.md new file mode 100644 index 000000000000..6d0894956cf7 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/mutations.md @@ -0,0 +1,17 @@ +--- +title: Mutations +intro: The mutation type defines GraphQL operations that create, update, or delete data on the server. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About mutations + +Every GraphQL schema has a root type for both queries and mutations. The mutation type defines GraphQL operations that modify data on the server. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/objects.md b/src/fixtures/fixtures/content/graphql/reference/objects.md new file mode 100644 index 000000000000..074cd5b87bdc --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/objects.md @@ -0,0 +1,17 @@ +--- +title: Objects +intro: Objects in GraphQL represent the resources you can access. An object can contain a list of fields, which are specifically typed. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About objects + +Objects are the most common type in a GraphQL schema. They represent the resources you can access and contain fields that define the data you can query. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/queries.md b/src/fixtures/fixtures/content/graphql/reference/queries.md new file mode 100644 index 000000000000..10427d7cf754 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/queries.md @@ -0,0 +1,17 @@ +--- +title: Queries +intro: The query type defines GraphQL operations that retrieve data from the server. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About queries + +Every GraphQL schema has a root type for both queries and mutations. The query type defines GraphQL operations that retrieve data from the server. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/scalars.md b/src/fixtures/fixtures/content/graphql/reference/scalars.md new file mode 100644 index 000000000000..939b3a6ec6f8 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/scalars.md @@ -0,0 +1,17 @@ +--- +title: Scalars +intro: Scalars are primitive values in GraphQL. They represent the leaves of a query. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About scalars + +Scalars are primitive types that represent concrete values. GraphQL comes with a set of default scalar types, including String, Int, Float, Boolean, and ID. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/graphql/reference/unions.md b/src/fixtures/fixtures/content/graphql/reference/unions.md new file mode 100644 index 000000000000..49c919383ca0 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/reference/unions.md @@ -0,0 +1,17 @@ +--- +title: Unions +intro: Unions represent an object that could be one of multiple types. +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- + +## About unions + +A union type is a special type that represents an object that could be one of a listed set of types. When you query a field that returns a union type, you need to use conditional fragments to query any fields. + +For more information, see [AUTOTITLE](/get-started/start-your-journey/hello-world). + + \ No newline at end of file diff --git a/src/fixtures/fixtures/content/index.md b/src/fixtures/fixtures/content/index.md index e3b95a0713d8..7b817882b23d 100644 --- a/src/fixtures/fixtures/content/index.md +++ b/src/fixtures/fixtures/content/index.md @@ -32,6 +32,7 @@ children: - actions - rest - webhooks + - graphql - video-transcripts # - account-and-profile # - authentication diff --git a/src/products/README.md b/src/products/README.md index 129c5bd19973..4ed3be956fcd 100644 --- a/src/products/README.md +++ b/src/products/README.md @@ -1 +1,46 @@ -❤︎ +# Products + +Utilities that read product metadata from `content/index.md`, build the product map used for navigation and context, and help group products for the home page. This README covers purpose, contracts, usage, workflows, testing, and ownership for the `src/products` subject. + +## Purpose & scope +* Generate a typed `productMap` keyed by product ID for routing, context middleware, and robots blocking. +* Derive product group data for the homepage hero/cards, with localization support. +* Provide stable product name mappings (e.g., Enterprise Server releases) for rendering and telemetry. +* Excludes: authoring guidance for product content (see `content/`), TOC authoring, and versioning docs (see `src/versions`). + +## Architecture & key assets +* `lib/all-products.ts`: Reads `content/index.md` frontmatter, builds `productMap` (id, name, href, dir, toc, wip, hidden, versions, external) and `productIds`; mutates later via middleware to add `nameRendered`. +* `lib/get-product-groups.ts`: Builds homepage product group structures; resolves localized names via translated `content/index.md` when present. +* `lib/product-names.ts`: Maps product codes (dotcom, GHES versions) to display names. +* `lib/old-developer-products.json`: Legacy list used for migration/compat checks. +* Tests under `tests/` validate schemas, product map shape, group helpers, and name mappings. + +## Data contracts +* Source: `content/index.md` frontmatter. + * `children`: array of product IDs (directory names under `content/`). + * `childGroups`: array of groups `{ name, icon?, octicon?, children[] }` for homepage cards; children entries can be product IDs or deeper paths. + * `externalProducts` (optional): map of product IDs to product objects matching `Product` shape; used to surface external docs. +* Product object shape (see `Product` in `lib/all-products.ts`): + * `id`, `name` (title or shortTitle), `href` (first applicable version), `dir`, `toc`, `wip`, `hidden`, `versions` (computed), optional `external`, optional `nameRendered` (added by middleware). +* Localization: For product groups, localized `content/index.md` frontmatter (same keys) is read if available; structure is always taken from English, names can be swapped via `octicon` matching. + +## Usage +* Server context middleware loads `productMap` into `req.context` for routing, version checks, and rendering product names. +* `get-product-groups` is used to render homepage product cards with localized names when available. +* `productMap` drives robots blocking (wip/hidden), nav generation (`get-toc-items`), and page validation (parent product assertions). + +## Workflows +* Update `content/index.md` to add or reorder products and product groups; ensure new product directories have `index.md` with frontmatter (`title`/`shortTitle`, `versions`, optional `wip`/`hidden`). +* To add an external product, define it under `externalProducts` in `content/index.md`. External products should include as many `Product` fields as applicable (such as `id`, `name`, `href`), but some fields (like `versions`) may be omitted. +* For localized group names, add translated `content/index.md` with matching `childGroups` and `octicon` keys. + +## Testing +* Full subject tests: `npm test -- src/products/tests` +* Targeted: + * `products.ts` validates `productMap` schema and presence of expected product IDs. + * `get-product-groups.ts` covers group helper mapping and localization. + * `product-names.ts` checks display name mappings. + +## Ownership & current state +* Owners: Docs Engineering. Content changes (titles, grouping, product lists) coordinated with docs-content. +* State: KTLO; update when products are added/retired or homepage groups change. \ No newline at end of file diff --git a/src/search/components/input/SearchContext.tsx b/src/search/components/input/SearchContext.tsx new file mode 100644 index 000000000000..b1fe3ae3c137 --- /dev/null +++ b/src/search/components/input/SearchContext.tsx @@ -0,0 +1,54 @@ +import { createContext, useContext, RefObject, SetStateAction, MutableRefObject } from 'react' +import type { AIReference } from '../types' +import type { AutocompleteSearchHit, GeneralSearchHit } from '@/search/types' + +export interface AutocompleteSearchHitWithUserQuery extends AutocompleteSearchHit { + isUserQuery?: boolean +} + +export interface GeneralSearchHitWithOptions extends GeneralSearchHit { + isViewAllResults?: boolean + isNoResultsFound?: boolean + isSearchDocsOption?: boolean +} + +export interface AskAIState { + isAskAIState: boolean + aiQuery: string + debug: boolean + currentVersion: string + setAISearchError: (isError?: boolean) => void + references: AIReference[] + setReferences: (value: SetStateAction) => void + referencesIndexOffset: number + referenceOnSelect: (url: string) => void + askAIEventGroupId: MutableRefObject + aiSearchError: boolean + aiCouldNotAnswer: boolean + setAICouldNotAnswer: (value: boolean) => void +} + +export interface SearchContextType { + t: any + generalSearchOptions: GeneralSearchHitWithOptions[] + aiOptionsWithUserInput: AutocompleteSearchHitWithUserQuery[] + generalSearchResultOnSelect: (selectedOption: GeneralSearchHit) => void + aiAutocompleteOnSelect: (selectedOption: AutocompleteSearchHit) => void + performGeneralSearch: () => void + selectedIndex: number + listElementsRef: RefObject> + askAIState: AskAIState + showSpinner: boolean + searchLoading: boolean + previousSuggestionsListHeight: number | string +} + +export const SearchContext = createContext(null) + +export const useSearchContext = () => { + const context = useContext(SearchContext) + if (!context) { + throw new Error('useSearchContext must be used within a SearchContext.Provider') + } + return context +} diff --git a/src/search/components/input/SearchGroups.tsx b/src/search/components/input/SearchGroups.tsx new file mode 100644 index 000000000000..1f9adac0e07d --- /dev/null +++ b/src/search/components/input/SearchGroups.tsx @@ -0,0 +1,251 @@ +import React from 'react' +import { ActionList, Spinner } from '@primer/react' +import { + SearchIcon, + FileIcon, + ArrowRightIcon, + CopilotIcon, + CommentIcon, +} from '@primer/octicons-react' + +import { AskAIResults } from './AskAIResults' +import { useSearchContext, AutocompleteSearchHitWithUserQuery } from './SearchContext' +import styles from './SearchOverlay.module.scss' + +export function SearchGroups() { + const { + t, + generalSearchOptions, + aiOptionsWithUserInput, + generalSearchResultOnSelect, + aiAutocompleteOnSelect, + performGeneralSearch, + selectedIndex, + listElementsRef, + askAIState, + showSpinner, + searchLoading, + previousSuggestionsListHeight, + } = useSearchContext() + + const isInAskAIState = askAIState?.isAskAIState && !askAIState.aiSearchError + const isInAskAIStateButNoAnswer = isInAskAIState && askAIState.aiCouldNotAnswer + + // This spinner is for both the AI search and the general search results. + // We already show a spinner when streaming AI response, so don't want to show 2 here + if (showSpinner && !isInAskAIState) { + return ( +
+ +
+ ) + } + + const groups = [] + + // We want to show general search suggestions above the AI Response section if the AI could not answer + if (generalSearchOptions.length || isInAskAIStateButNoAnswer) { + const items = [] + for (let index = 0; index < generalSearchOptions.length; index++) { + const option = generalSearchOptions[index] + if (option.isNoResultsFound) { + items.push( + + {option.title} + , + ) + // There should be no more items after the no results found item + break + // This is a special case where there is an error loading search results and we want to be able to search the docs using the user's query + } else if (option.isSearchDocsOption) { + const isActive = selectedIndex === index + items.push( + performGeneralSearch()} + aria-label={t('search.overlay.search_docs_with_query').replace('{query}', option.title)} + ref={(element: HTMLLIElement | null) => { + if (listElementsRef.current) { + listElementsRef.current[index] = element + } + }} + > + + + + {option.title} + + + + , + ) + } else if (option.title) { + const isActive = selectedIndex === index + items.push( + + option.isViewAllResults ? performGeneralSearch() : generalSearchResultOnSelect(option) + } + className={option.isViewAllResults ? styles.viewAllSearchResults : ''} + active={isActive} + tabIndex={-1} + ref={(element: HTMLLIElement | null) => { + if (listElementsRef.current) { + listElementsRef.current[index] = element + } + }} + > + {!option.isNoResultsFound && ( + + + + )} + {option.title} + + + + , + ) + } + } + + groups.push( + + + {t('search.overlay.general_suggestions_list_heading')} + + {searchLoading && isInAskAIState ? ( +
+ +
+ ) : ( + items + )} +
, + ) + + if (isInAskAIState || isInAskAIStateButNoAnswer) { + groups.push() + } + + if (isInAskAIState) { + groups.push( + +
  • + +
  • +
    , + ) + } + + // Don't show the bottom divider if: + // 1. We are in the AI could not answer state + // 2. We are in the AI Search error state + // 3. There are no AI suggestions to show in suggestions state + if ( + !isInAskAIState && + !askAIState.aiSearchError && + generalSearchOptions.filter((option) => !option.isViewAllResults && !option.isNoResultsFound) + .length && + aiOptionsWithUserInput.length + ) { + groups.push() + } + } + + if (aiOptionsWithUserInput.length && !isInAskAIState) { + groups.push( + + + + {t('search.overlay.ai_autocomplete_list_heading')} + + {aiOptionsWithUserInput.map((option: AutocompleteSearchHitWithUserQuery, index: number) => { + // Since general search comes first, we need to add an offset for AI suggestions + const indexWithOffset = generalSearchOptions.length + index + const isActive = selectedIndex === indexWithOffset + const item = ( + aiAutocompleteOnSelect(option)} + active={isActive} + tabIndex={-1} + ref={(element: HTMLLIElement | null) => { + if (listElementsRef.current) { + listElementsRef.current[indexWithOffset] = element + } + }} + > + + + + {option.term} + + + + + ) + return item + })} + , + ) + } + + return <>{groups} +} diff --git a/src/search/components/input/SearchOverlay.tsx b/src/search/components/input/SearchOverlay.tsx index 8ab7fd39af7e..a1e42fa5aa87 100644 --- a/src/search/components/input/SearchOverlay.tsx +++ b/src/search/components/input/SearchOverlay.tsx @@ -1,16 +1,8 @@ -import React, { useState, useRef, RefObject, useEffect, SetStateAction, useMemo } from 'react' +import React, { useState, useRef, RefObject, useEffect, useMemo } from 'react' import cx from 'classnames' import { useRouter } from 'next/router' -import { ActionList, IconButton, Overlay, Spinner, Stack, TextInput, Banner } from '@primer/react' -import { - SearchIcon, - XCircleFillIcon, - CommentIcon, - CopilotIcon, - FileIcon, - ArrowRightIcon, - ArrowLeftIcon, -} from '@primer/octicons-react' +import { ActionList, IconButton, Overlay, Stack, TextInput, Banner } from '@primer/react' +import { SearchIcon, XCircleFillIcon, CopilotIcon, ArrowLeftIcon } from '@primer/octicons-react' import { focusTrap } from '@primer/behaviors' import { useTranslation } from '@/languages/components/useTranslation' @@ -21,7 +13,6 @@ import { GENERAL_SEARCH_CONTEXT, } from '../helpers/execute-search-actions' import { useCombinedSearchResults } from '@/search/components/hooks/useAISearchAutocomplete' -import { AskAIResults } from './AskAIResults' import { sendEvent, uuidv4 } from '@/events/components/events' import { EventType } from '@/events/types' import { ASK_AI_EVENT_GROUP, SEARCH_OVERLAY_EVENT_GROUP } from '@/events/components/event-groups' @@ -32,6 +23,13 @@ import type { AutocompleteSearchHit, GeneralSearchHit } from '@/search/types' import { sanitizeSearchQuery } from '@/search/lib/sanitize-search-query' +import { + SearchContext, + SearchContextType, + AutocompleteSearchHitWithUserQuery, + GeneralSearchHitWithOptions, +} from './SearchContext' +import { SearchGroups } from './SearchGroups' import styles from './SearchOverlay.module.scss' type Props = { @@ -532,7 +530,6 @@ export function SearchOverlay({ } // We render the AI Result in the searchGroups call, so we pass the props down via an object - // TODO: Move stateful logic to Context since we now have so many props: const askAIState = { isAskAIState, aiQuery, @@ -556,6 +553,21 @@ export function SearchOverlay({ setAICouldNotAnswer, } + const searchContextValue: SearchContextType = { + t, + generalSearchOptions: generalOptionsWithViewStatus, + aiOptionsWithUserInput, + generalSearchResultOnSelect, + aiAutocompleteOnSelect: aiSearchOptionOnSelect, + performGeneralSearch, + selectedIndex, + listElementsRef: listElementsRef as RefObject>, + askAIState, + showSpinner, + searchLoading, + previousSuggestionsListHeight, + } + // We display different content in the overlay based: // 1. If either search (autocomplete results or ask AI) has an error // 2. The user has selected an AI query and we are showing the ask AI results @@ -566,7 +578,12 @@ export function SearchOverlay({ const inErrorState = aiSearchError || (autoCompleteSearchError && !isAskAIState) if (inErrorState) { OverlayContents = ( - <> + )} - {renderSearchGroups( - t, - generalOptionsWithViewStatus, - aiSearchError ? [] : aiOptionsWithUserInput, - generalSearchResultOnSelect, - aiSearchOptionOnSelect, - performGeneralSearch, - selectedIndex, - listElementsRef, - askAIState, - showSpinner, - searchLoading, - previousSuggestionsListHeight, - )} + - + ) } else { OverlayContents = ( - - {renderSearchGroups( - t, - generalOptionsWithViewStatus, - aiOptionsWithUserInput, - generalSearchResultOnSelect, - aiSearchOptionOnSelect, - performGeneralSearch, - selectedIndex, - listElementsRef, - askAIState, - showSpinner, - searchLoading, - previousSuggestionsListHeight, - )} - + + + + + ) } @@ -759,268 +752,6 @@ export function SearchOverlay({ ) } -interface AutocompleteSearchHitWithUserQuery extends AutocompleteSearchHit { - isUserQuery?: boolean -} - -interface GeneralSearchHitWithOptions extends GeneralSearchHit { - isViewAllResults?: boolean - isNoResultsFound?: boolean - isSearchDocsOption?: boolean -} - -// Render the autocomplete suggestions with AI suggestions first, headings, and a divider between the two -function renderSearchGroups( - t: any, - generalSearchOptions: GeneralSearchHitWithOptions[], - aiOptionsWithUserInput: AutocompleteSearchHitWithUserQuery[], - generalSearchResultOnSelect: (selectedOption: GeneralSearchHit) => void, - aiAutocompleteOnSelect: (selectedOption: AutocompleteSearchHit) => void, - performGeneralSearch: () => void, - selectedIndex: number, - listElementsRef: RefObject>, - askAIState: { - isAskAIState: boolean - aiQuery: string - debug: boolean - currentVersion: string - setAISearchError: () => void - references: AIReference[] - setReferences: (value: SetStateAction) => void - referencesIndexOffset: number - referenceOnSelect: (url: string) => void - askAIEventGroupId: React.MutableRefObject - aiSearchError: boolean - aiCouldNotAnswer: boolean - setAICouldNotAnswer: (value: boolean) => void - }, - showSpinner: boolean, - searchLoading: boolean, - previousSuggestionsListHeight: number | string, -) { - const groups = [] - - const isInAskAIState = askAIState?.isAskAIState && !askAIState.aiSearchError - const isInAskAIStateButNoAnswer = isInAskAIState && askAIState.aiCouldNotAnswer - - // This spinner is for both the AI search and the general search results. - // We already show a spinner when streaming AI response, so don't want to show 2 here - if (showSpinner && !isInAskAIState) { - groups.push( -
    - -
    , - ) - return groups - } - - // We want to show general search suggestions above the AI Response section if the AI could not answer - if (generalSearchOptions.length || isInAskAIStateButNoAnswer) { - const items = [] - for (let index = 0; index < generalSearchOptions.length; index++) { - const option = generalSearchOptions[index] - if (option.isNoResultsFound) { - items.push( - - {option.title} - , - ) - // There should be no more items after the no results found item - break - // This is a special case where there is an error loading search results and we want to be able to search the docs using the user's query - } else if (option.isSearchDocsOption) { - const isActive = selectedIndex === index - items.push( - performGeneralSearch()} - aria-label={t('search.overlay.search_docs_with_query').replace('{query}', option.title)} - ref={(element: HTMLLIElement | null) => { - if (listElementsRef.current) { - listElementsRef.current[index] = element - } - }} - > - - - - {option.title} - - - - , - ) - } else if (option.title) { - const isActive = selectedIndex === index - items.push( - - option.isViewAllResults ? performGeneralSearch() : generalSearchResultOnSelect(option) - } - className={option.isViewAllResults ? styles.viewAllSearchResults : ''} - active={isActive} - tabIndex={-1} - ref={(element: HTMLLIElement | null) => { - if (listElementsRef.current) { - listElementsRef.current[index] = element - } - }} - > - {!option.isNoResultsFound && ( - - - - )} - {option.title} - - - - , - ) - } - } - - groups.push( - - - {t('search.overlay.general_suggestions_list_heading')} - - {searchLoading && isInAskAIState ? ( -
    - -
    - ) : ( - items - )} -
    , - ) - - if (isInAskAIState || isInAskAIStateButNoAnswer) { - groups.push() - } - - if (isInAskAIState) { - groups.push( - -
  • - -
  • -
    , - ) - } - - // Don't show the bottom divider if: - // 1. We are in the AI could not answer state - // 2. We are in the AI Search error state - // 3. There are no AI suggestions to show in suggestions state - if ( - !isInAskAIState && - !askAIState.aiSearchError && - generalSearchOptions.filter((option) => !option.isViewAllResults && !option.isNoResultsFound) - .length && - aiOptionsWithUserInput.length - ) { - groups.push() - } - } - - if (aiOptionsWithUserInput.length && !isInAskAIState) { - groups.push( - - - - {t('search.overlay.ai_autocomplete_list_heading')} - - {aiOptionsWithUserInput.map((option: AutocompleteSearchHitWithUserQuery, index: number) => { - // Since general search comes first, we need to add an offset for AI suggestions - const indexWithOffset = generalSearchOptions.length + index - const isActive = selectedIndex === indexWithOffset - const item = ( - aiAutocompleteOnSelect(option)} - active={isActive} - tabIndex={-1} - ref={(element: HTMLLIElement | null) => { - if (listElementsRef.current) { - listElementsRef.current[index] = element - } - }} - > - - - - {option.term} - - - - - ) - return item - })} - , - ) - } - - return groups -} - function sendKeyboardEvent( pressedKey: string, pressedOn: string, diff --git a/src/types/types.ts b/src/types/types.ts index 343ce700ab1c..74e2819256f6 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -388,6 +388,7 @@ export type Page = { sidebarLink?: SidebarLink type?: string contentType?: string + children?: string[] } export type SidebarLink = { diff --git a/tsconfig.json b/tsconfig.json index d6800988c957..5da89e0e021b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,8 @@ "exclude": [ "node_modules", "docs-internal-data", - "src/code-scanning/scripts/generate-code-scanning-query-list.ts" + "src/codeql-queries/scripts/generate-code-scanning-query-list.ts", + "src/codeql-queries/scripts/generate-code-quality-query-list.ts" ], "include": [ "**/*.ts",