CodeQL for VS Code: Initial commit.
Some checks failed
Build Extension / Build (ubuntu-latest) (push) Has been cancelled
Build Extension / Build (windows-latest) (push) Has been cancelled
Build Extension / Test (ubuntu-latest) (push) Has been cancelled
Build Extension / Test (windows-latest) (push) Has been cancelled
Release / Release (push) Has been cancelled

This commit is contained in:
Aditya Sharad
2019-11-12 11:48:25 -08:00
commit d9a1dce7fa
133 changed files with 22112 additions and 0 deletions

6
.editorconfig Normal file
View File

@@ -0,0 +1,6 @@
[*.{ts,tsx,css,js,json}]
indent_style = space
indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true

14
.gitattributes vendored Normal file
View File

@@ -0,0 +1,14 @@
# Don't allow people to merge changes to these generated files, because the result
# may be invalid. You need to run "rush update" again.
pnpm-lock.yaml merge=binary
shrinkwrap.yaml merge=binary
npm-shrinkwrap.json merge=binary
yarn.lock merge=binary
# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic
# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor
# may also require a special configuration to allow comments in JSON.
#
# For more information, see this issue: https://github.com/Microsoft/web-build-tools/issues/1088
#
*.json linguist-language=JSON-with-Comments

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

@@ -0,0 +1,73 @@
name: Build Extension
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
- name: Build
run: |
cd build
npm install
npm run build-ci
shell: bash
- name: Prepare artifacts
if: matrix.os == 'ubuntu-latest'
run: |
mkdir artifacts
cp dist/*.vsix artifacts
- name: Upload artifacts
uses: actions/upload-artifact@master
if: matrix.os == 'ubuntu-latest'
with:
name: vscode-codeql-extension
path: artifacts
test:
name: Test
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v1
with:
fetch-depth: 1
# We have to build the dependencies in `lib` before running any tests.
- name: Build
run: |
cd build
npm install
npm run build-ci
shell: bash
- name: Run unit tests
run: |
cd extensions/ql-vscode
npm run test
- name: Run integration tests (Linux)
if: matrix.os == 'ubuntu-latest'
run: |
cd extensions/ql-vscode
sudo apt-get install xvfb
/usr/bin/xvfb-run npm run integration
- name: Run integration tests (Windows)
if: matrix.os == 'windows-latest'
run: |
cd extensions/ql-vscode
npm run integration

90
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
# Build and release a new version of the extension.
# Based on example workflow at https://github.com/actions/upload-release-asset
# licensed under https://github.com/actions/upload-release-asset/blob/master/LICENSE.
# Reference for passing data between steps:
# https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
name: Release
on:
push:
# Path filters are not evaluated for pushes to tags.
# (source: https://help.github.com/en/github/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions#onpushpull_requestpaths)
# So this workflow is triggered in the following events:
# - Release event: a SemVer tag, e.g. v1.0.0 or v1.0.0-alpha, is pushed
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
# OR
# - Test event: this file is modified on a branch in the main repo containing `/actions/` in the name.
branches:
- '**/actions/**'
paths:
- '**/workflows/release.yml'
jobs:
build:
name: Release
runs-on: ubuntu-latest
# TODO Share steps with the main workflow.
steps:
- name: Checkout
uses: actions/checkout@master
- name: Build
run: |
cd build
npm install
# Release build instead of dev build.
npm run build-release
shell: bash
- name: Prepare artifacts
id: prepare-artifacts
run: |
mkdir artifacts
cp dist/*.vsix artifacts
# Record the VSIX path as an output of this step.
# This will be used later when uploading a release asset.
VSIX_PATH="$(ls dist/*.vsix)"
echo "::set-output name=vsix_path::$VSIX_PATH"
# Transform the GitHub ref so it can be used in a filename.
# This is mainly needed for testing branches that modify this workflow.
REF_NAME="$(echo ${{ github.ref }} | sed -e 's:/:-:g')"
echo "::set-output name=ref_name::$REF_NAME"
# Uploading artifacts is not necessary to create a release.
# This is just in case the release itself fails and we want to access the built artifacts from Actions.
# TODO Remove if not useful.
- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: vscode-codeql-extension
path: artifacts
# TODO Run tests, or check that a test run on the same branch succeeded.
- name: Create release
id: create-release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
# This gives us a chance to manually review the created release before publishing it,
# as well as to test the release workflow by pushing temporary tags.
# Once we have set all required release metadata in this step, we can set this to `false`.
draft: true
prerelease: false
- name: Upload release asset
uses: actions/upload-release-asset@v1.0.1
if: success()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Get the `upload_url` from the `create-release` step above.
upload_url: ${{ steps.create-release.outputs.upload_url }}
# Get the `vsix_path` and `ref_name` from the `prepare-artifacts` step above.
asset_path: ${{ steps.prepare-artifacts.outputs.vsix_path }}
asset_name: ${{ format('vscode-codeql-{0}.vsix', steps.prepare-artifacts.outputs.ref_name) }}
asset_content_type: application/zip

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
# Logs
*.log
# Generated files
/dist/
out/
server/
node_modules/
gen/
# Integration test artifacts
**/.vscode-test/**
# Visual Studio workspace state
.vs/
# Rush files
/common/temp/**
package-deps.json

10
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
// List of extensions which should be recommended for users of this workspace.
"recommendations": [
"eamodio.tsl-problem-matcher"
],
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
"unwantedRecommendations": []
}

84
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,84 @@
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension (vscode-codeql)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--disable-extensions"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/dist/vscode-codeql/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-bqrs/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io-node/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-vscode-utils/out/**/*.js"
],
"preLaunchTask": "Build"
},
{
"name": "Launch Unit Tests (vscode-codeql)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/test",
"--disable-extensions"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/dist/vscode-codeql/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-bqrs/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-io-node/out/**/*.js",
"${workspaceRoot}/dist/vscode-codeql/node_modules/semmle-vscode-utils/out/**/*.js",
"${workspaceRoot}/extensions/ql-vscode/out/test/**/*.js"
],
"preLaunchTask": "Build"
},
{
"name": "Launch Integration Tests - No Workspace (vscode-codeql)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/no-workspace/index",
"--disable-extensions"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/dist/vscode-codeql/out/**/*.js",
"${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/**/*.js"
],
"preLaunchTask": "Build"
},
{
"name": "Launch Integration Tests - Minimal Workspace (vscode-codeql)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceRoot}/dist/vscode-codeql",
"--extensionTestsPath=${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/minimal-workspace/index",
"${workspaceRoot}/extensions/ql-vscode/test/data",
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/dist/vscode-codeql/out/**/*.js",
"${workspaceRoot}/extensions/ql-vscode/out/vscode-tests/**/*.js"
],
"preLaunchTask": "Build"
}
]
}

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

@@ -0,0 +1,14 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"files.watcherExclude": {
"**/.git/**": true,
"**/node_modules/*/**": true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
"typescript.tsdk": "./common/temp/node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version
}

105
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,105 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Build",
"type": "shell",
"group": {
"kind": "build",
"isDefault": true
},
"command": "node common/scripts/install-run-rush.js build --verbose",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"problemMatcher": [
{
"owner": "typescript",
"fileLocation": "absolute",
"pattern": {
"regexp": "^\\[gulp-typescript\\] ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): error TS\\d+: (.*)$",
"file": 1,
"location": 2,
"message": 3
},
},
"$ts-webpack"
]
},
{
"label": "Rebuild",
"type": "shell",
"group": "build",
"command": "node common/scripts/install-run-rush.js rebuild --verbose",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"problemMatcher": [
{
"owner": "typescript",
"fileLocation": "absolute",
"pattern": {
"regexp": "^\\[gulp-typescript\\] ([^(]+)\\((\\d+|\\d+,\\d+|\\d+,\\d+,\\d+,\\d+)\\): error TS\\d+: (.*)$",
"file": 1,
"location": 2,
"message": 3
}
}
]
},
{
"label": "Update",
"type": "shell",
"command": "node common/scripts/install-run-rush.js update",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"problemMatcher": []
},
{
"label": "Update (full)",
"type": "shell",
"command": "node common/scripts/install-run-rush.js update --full",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"problemMatcher": []
},
{
"label": "Format",
"type": "shell",
"command": "node common/scripts/install-run-rush.js format",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true
},
"problemMatcher": []
}
]
}

76
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at opensource@github.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

135
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,135 @@
## Contributing
[fork]: https://github.com/github/vscode-codeql/fork
[pr]: https://github.com/github/vscode-codeql/compare
[style]: https://primer.style
[code-of-conduct]: CODE_OF_CONDUCT.md
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE.md).
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
## Submitting a pull request
0. [Fork][fork] and clone the repository
0. Set up a local build
0. Create a new branch: `git checkout -b my-branch-name`
0. Make your change
0. Push to your fork and [submit a pull request][pr]
0. Pat yourself on the back and wait for your pull request to be reviewed and merged.
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Follow the [style guide][style].
- Write tests. Tests that don't require the VS Code API are located [here](extensions/ql-vscode/test). Integration tests that do require the VS Code API are located [here](extensions/ql-vscode/src/vscode-tests).
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
## Setting up a local build
Make sure you have a fairly recent version of vscode (>1.32) and are using nodejs
version >=v10.13.0. (Tested on v10.15.1 and v10.16.0).
This repo uses [Rush](https://rushjs.io) to handle package management, building, and other
operations across multiple projects. See the Rush "[Getting started as a developer](https://rushjs.io/pages/developer/new_developer/)" docs
for more details.
If you plan on building from the command line, it's easiest if Rush is installed globally:
```shell
npm install -g @microsoft/rush
```
Note that when you run the `rush` command from the globally installed version, it will examine the
`rushVersion` property in the repo's `rush.json`, and if it differs from the globally installed
version, it will download, cache, and run the version of Rush specified in the `rushVersion`
property.
If you plan on only building via VS Code tasks, you don't need Rush installed at all, since those
tasks run `common/scripts/install-run-rush.js` to bootstrap a locally installed and cached copy of
Rush.
### Building
#### Installing all packages (instead of `npm install`)
After updating any `package.json` file, or after checking or pulling a new branch, you need to
make sure all the right npm packages are installed, which you would normally do via `npm install` in
a single-project repo. With Rush, you need to do an "update" instead:
##### From VS Code
`Terminal > Run Task... > Update`
##### From the command line
```shell
$ rush update
```
#### Building all projects (instead of `gulp`)
Rush builds all projects in the repo, in dependency order, building multiple projects in parallel
where possible. By default, the build also packages the extension itself into a .vsix file in the
`dist` directory. To build:
##### From VS Code
`Terminal > Run Build Task...` (or just `Ctrl+Shift+B` with the default key bindings)
##### From the command line
```shell
rush build --verbose
```
#### Forcing a clean build
Rush does a reasonable job of detecting on its own which projects need to be rebuilt, but if you need to
force a full rebuild of all projects:
##### From VS Code
`Terminal > Run Task... > Rebuild`
##### From the command line
```shell
rush rebuild --verbose
```
### Installing
You can install the `.vsix` file from within VS Code itself, from the Extensions container in the sidebar:
`More Actions...` (top right) `> Install from VSIX...`
Or, from the command line, use something like (depending on where you have VSCode installed):
```shell
$ code --install-extension dist/vscode-codeql-*.vsix # normal VSCode installation
# or maybe
$ vscode/scripts/code-cli.sh --install-extension dist/vscode-codeql-*.vsix # if you're using the open-source version from a checkout of https://github.com/microsoft/vscode
```
### Debugging
You can use VS Code to debug the extension without explicitly installing it. Just open this directory as a workspace in VS Code, and hit `F5` to start a debugging session.
## Releasing (write access required)
1. Trigger a release build on Actions by adding a new tag on master of the format `vxx.xx.xx`
1. Monitor the status of the release build in the `Release` workflow in the Actions tab.
1. Download the VSIX from the draft GitHub release that is created when the release build finishes.
1. Log into the [Visual Studio Marketplace](https://marketplace.visualstudio.com/manage/publishers/github).
1. Click the `...` menu in the CodeQL row and click **Update**.
1. Drag the `.vsix` file you downloaded from the GitHub release into the Marketplace and click **Upload**.
1. Publish the GitHub release.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)

20
LICENSE.md Normal file
View File

@@ -0,0 +1,20 @@
Copyright (c) 2019 GitHub, Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# CodeQL for Visual Studio Code
This project is an extension for Visual Studio Code that adds rich language support for CodeQL. It's used to find problems in code bases using CodeQL. It's written primarily in TypeScript.
The extension is released. You can download it from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql).
![CI status badge](https://github.com/github/vscode-codeql/workflows/Build%20Extension/badge.svg)
## Features
* Enables you to use CodeQL to query databases and discover problems in codebases.
* Shows the flow of data through the results of path queries, which is essential for triaging security results.
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/Semmle/ql).
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
## Project goals and scope
This project will track new feature development in CodeQL and, whenever appropriate, bring that functionality to the Visual Studio Code experience.
## Contributing
This project welcomes contributions. See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to build, install, and contribute.
## License
The CodeQL extension for Visual Studio Code is [licensed](LICENSE.md) under the MIT License. The version of CodeQL used by the CodeQL extension is subject to the [CodeQL Research Terms & Conditions](https://securitylab.github.com/tools/codeql/license).
When using the GitHub logos, be sure to follow the [GitHub logo guidelines](https://github.com/logos).

12
build/README.md Normal file
View File

@@ -0,0 +1,12 @@
GitHub Actions Build directory
===
The point of this directory is to allow us to do a local installation *of* the rush
tool, since
- installing globally is not permitted on github actions
- installing locally in the root directory of the repo creates `node_modules` there,
and rush itself gives error messages since it thinks `node_modules` is not supposed
to exist, since rush is supposed to be managing subproject dependencies.
Running rush from a subdirectory searches parent directories for `rush.json`
and does the build starting from that file's location.

1294
build/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
build/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "build",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@microsoft/rush": "^5.10.3"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rush update && rush build",
"build-ci": "rush install && rush build",
"build-release": "rush install && rush build --release"
},
"author": "GitHub"
}

28
common/config/LICENSE Normal file
View File

@@ -0,0 +1,28 @@
This directory contains content from https://github.com/microsoft/rushstack,
used under the MIT license as follows.
See https://github.com/microsoft/rushstack/blob/master/stack/rush-stack/LICENSE.
@microsoft/rush-stack
Copyright (c) Microsoft Corporation. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

12
common/config/rush/.npmrc Normal file
View File

@@ -0,0 +1,12 @@
# Rush uses this file to configure the package registry, regardless of whether the
# package manager is PNPM, NPM, or Yarn. Prior to invoking the package manager,
# Rush will always copy this file to the folder where installation is performed.
# When NPM is the package manager, Rush works around NPM's processing of
# undefined environment variables by deleting any lines that reference undefined
# environment variables.
#
# DO NOT SPECIFY AUTHENTICATION CREDENTIALS IN THIS FILE. It should only be used
# to configure registry sources.
registry=https://registry.npmjs.org/
always-auth=false

View File

@@ -0,0 +1,32 @@
/**
* This configuration file defines custom commands for the "rush" command-line.
* For full documentation, please see https://rushjs.io/pages/configs/command_line_json/
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json",
"commands": [
{
"commandKind": "bulk",
"name": "format",
"summary": "Reformat source code in all projects",
"description": "Runs the `format` npm task in each project, if present.",
"safeForSimultaneousRushProcesses": false,
"enableParallelism": true,
"ignoreDependencyOrder": true,
"ignoreMissingScript": true,
"allowWarningsInSuccessfulBuild": false
}
],
"parameters": [
{
"parameterKind": "flag",
"longName": "--release",
"shortName": "-r",
"description": "Perform a release build",
"associatedCommands": [
"build",
"rebuild"
],
}
]
}

View File

@@ -0,0 +1,43 @@
/**
* This configuration file specifies NPM dependency version selections that affect all projects
* in a Rush repo. For full documentation, please see https://rushjs.io
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json",
/**
* A table that specifies a "preferred version" for a dependency package. The "preferred version"
* is typically used to hold an indirect dependency back to a specific version, however generally
* it can be any SemVer range specifier (e.g. "~1.2.3"), and it will narrow any (compatible)
* SemVer range specifier. See the Rush documentation for details about this feature.
*/
"preferredVersions": {
/**
* When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo,
* instead of the latest version.
*/
// "some-library": "1.2.3"
},
/**
* The "rush check" command can be used to enforce that every project in the repo must specify
* the same SemVer range for a given dependency. However, sometimes exceptions are needed.
* The allowedAlternativeVersions table allows you to list other SemVer ranges that will be
* accepted by "rush check" for a given dependency.
*
* IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE
* USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO).
* This design avoids unnecessary churn in this file.
*/
"allowedAlternativeVersions": {
/**
* For example, allow some projects to use an older TypeScript compiler
* (in addition to whatever "usual" version is being used by other projects in the repo):
*/
// "typescript": [
// "~2.4.0"
// ]
}
}

View File

@@ -0,0 +1,32 @@
"use strict";
/**
* When using the PNPM package manager, you can use pnpmfile.js to workaround
* dependencies that have mistakes in their package.json file. (This feature is
* functionally similar to Yarn's "resolutions".)
*
* For details, see the PNPM documentation:
* https://pnpm.js.org/docs/en/hooks.html
*
* IMPORTANT: SINCE THIS FILE CONTAINS EXECUTABLE CODE, MODIFYING IT IS LIKELY
* TO INVALIDATE ANY CACHED DEPENDENCY ANALYSIS. We recommend to run "rush update --full"
* after any modification to pnpmfile.js.
*
*/
module.exports = {
hooks: {
readPackage
}
};
/**
* This hook is invoked during installation before a package's dependencies
* are selected.
* The `packageJson` parameter is the deserialized package.json
* contents for the package that is about to be installed.
* The `context` parameter provides a log() function.
* The return value is the updated object.
*/
function readPackage(packageJson, context) {
return packageJson;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
/**
* This is configuration file is used for advanced publishing configurations with Rush.
* For full documentation, please see https://rushjs.io/pages/configs/version_policies_json/
*/
[]

View File

@@ -0,0 +1,52 @@
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where the Rush command may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush
// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run-rush.js install
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
const path = require("path");
const fs = require("fs");
const install_run_1 = require("./install-run");
const PACKAGE_NAME = '@microsoft/rush';
function getRushVersion() {
const rushJsonFolder = install_run_1.findRushJsonFolder();
const rushJsonPath = path.join(rushJsonFolder, install_run_1.RUSH_JSON_FILENAME);
try {
const rushJsonContents = fs.readFileSync(rushJsonPath, 'utf-8');
// Use a regular expression to parse out the rushVersion value because rush.json supports comments,
// but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script.
const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/);
return rushJsonMatches[1];
}
catch (e) {
throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` +
'The \'rushVersion\' field is either not assigned in rush.json or was specified ' +
'using an unexpected syntax.');
}
}
function run() {
const [nodePath, /* Ex: /bin/node */ scriptPath, /* /repo/common/scripts/install-run-rush.js */ ...packageBinArgs /* [build, --to, myproject] */] = process.argv;
if (!nodePath || !scriptPath) {
throw new Error('Unexpected exception: could not detect node path or script path');
}
if (process.argv.length < 3) {
console.log('Usage: install-run-rush.js <command> [args...]');
console.log('Example: install-run-rush.js build --to myproject');
process.exit(1);
}
install_run_1.runWithErrorAndStatusCode(() => {
const version = getRushVersion();
console.log(`The rush.json configuration requests Rush version ${version}`);
return install_run_1.installAndRun(PACKAGE_NAME, version, 'rush', packageBinArgs);
});
}
run();
//# sourceMappingURL=install-run-rush.js.map

View File

@@ -0,0 +1,399 @@
"use strict";
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See the @microsoft/rush package's LICENSE file for license information.
Object.defineProperty(exports, "__esModule", { value: true });
// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED.
//
// This script is intended for usage in an automated build environment where a Node tool may not have
// been preinstalled, or may have an unpredictable version. This script will automatically install the specified
// version of the specified tool (if not already installed), and then pass a command-line to it.
// An example usage would be:
//
// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io
//
// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/
const childProcess = require("child_process");
const fs = require("fs");
const os = require("os");
const path = require("path");
exports.RUSH_JSON_FILENAME = 'rush.json';
const INSTALLED_FLAG_FILENAME = 'installed.flag';
const NODE_MODULES_FOLDER_NAME = 'node_modules';
const PACKAGE_JSON_FILENAME = 'package.json';
/**
* Parse a package specifier (in the form of name\@version) into name and version parts.
*/
function parsePackageSpecifier(rawPackageSpecifier) {
rawPackageSpecifier = (rawPackageSpecifier || '').trim();
const separatorIndex = rawPackageSpecifier.lastIndexOf('@');
let name;
let version = undefined;
if (separatorIndex === 0) {
// The specifier starts with a scope and doesn't have a version specified
name = rawPackageSpecifier;
}
else if (separatorIndex === -1) {
// The specifier doesn't have a version
name = rawPackageSpecifier;
}
else {
name = rawPackageSpecifier.substring(0, separatorIndex);
version = rawPackageSpecifier.substring(separatorIndex + 1);
}
if (!name) {
throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`);
}
return { name, version };
}
/**
* Resolve a package specifier to a static version
*/
function resolvePackageVersion(rushCommonFolder, { name, version }) {
if (!version) {
version = '*'; // If no version is specified, use the latest version
}
if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) {
// If the version contains only characters that we recognize to be used in static version specifiers,
// pass the version through
return version;
}
else {
// version resolves to
try {
const rushTempFolder = ensureAndJoinPath(rushCommonFolder, 'temp');
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
syncNpmrc(sourceNpmrcFolder, rushTempFolder);
const npmPath = getNpmPath();
// This returns something that looks like:
// @microsoft/rush@3.0.0 '3.0.0'
// @microsoft/rush@3.0.1 '3.0.1'
// ...
// @microsoft/rush@3.0.20 '3.0.20'
// <blank line>
const npmVersionSpawnResult = childProcess.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], {
cwd: rushTempFolder,
stdio: []
});
if (npmVersionSpawnResult.status !== 0) {
throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`);
}
const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString();
const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line);
const latestVersion = versionLines[versionLines.length - 1];
if (!latestVersion) {
throw new Error('No versions found for the specified version range.');
}
const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/);
if (!versionMatches) {
throw new Error(`Invalid npm output ${latestVersion}`);
}
return versionMatches[1];
}
catch (e) {
throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`);
}
}
}
let _npmPath = undefined;
/**
* Get the absolute path to the npm executable
*/
function getNpmPath() {
if (!_npmPath) {
try {
if (os.platform() === 'win32') {
// We're on Windows
const whereOutput = childProcess.execSync('where npm', { stdio: [] }).toString();
const lines = whereOutput.split(os.EOL).filter((line) => !!line);
// take the last result, we are looking for a .cmd command
// see https://github.com/Microsoft/web-build-tools/issues/759
_npmPath = lines[lines.length - 1];
}
else {
// We aren't on Windows - assume we're on *NIX or Darwin
_npmPath = childProcess.execSync('which npm', { stdio: [] }).toString();
}
}
catch (e) {
throw new Error(`Unable to determine the path to the NPM tool: ${e}`);
}
_npmPath = _npmPath.trim();
if (!fs.existsSync(_npmPath)) {
throw new Error('The NPM executable does not exist');
}
}
return _npmPath;
}
exports.getNpmPath = getNpmPath;
let _rushJsonFolder;
/**
* Find the absolute path to the folder containing rush.json
*/
function findRushJsonFolder() {
if (!_rushJsonFolder) {
let basePath = __dirname;
let tempPath = __dirname;
do {
const testRushJsonPath = path.join(basePath, exports.RUSH_JSON_FILENAME);
if (fs.existsSync(testRushJsonPath)) {
_rushJsonFolder = basePath;
break;
}
else {
basePath = tempPath;
}
} while (basePath !== (tempPath = path.dirname(basePath))); // Exit the loop when we hit the disk root
if (!_rushJsonFolder) {
throw new Error('Unable to find rush.json.');
}
}
return _rushJsonFolder;
}
exports.findRushJsonFolder = findRushJsonFolder;
/**
* Create missing directories under the specified base directory, and return the resolved directory.
*
* Does not support "." or ".." path segments.
* Assumes the baseFolder exists.
*/
function ensureAndJoinPath(baseFolder, ...pathSegments) {
let joinedPath = baseFolder;
try {
for (let pathSegment of pathSegments) {
pathSegment = pathSegment.replace(/[\\\/]/g, '+');
joinedPath = path.join(joinedPath, pathSegment);
if (!fs.existsSync(joinedPath)) {
fs.mkdirSync(joinedPath);
}
}
}
catch (e) {
throw new Error(`Error building local installation folder (${path.join(baseFolder, ...pathSegments)}): ${e}`);
}
return joinedPath;
}
/**
* As a workaround, _syncNpmrc() copies the .npmrc file to the target folder, and also trims
* unusable lines from the .npmrc file. If the source .npmrc file not exist, then _syncNpmrc()
* will delete an .npmrc that is found in the target folder.
*
* Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in
* the .npmrc file to provide different authentication tokens for different registry.
* However, if the environment variable is undefined, it expands to an empty string, which
* produces a valid-looking mapping with an invalid URL that causes an error. Instead,
* we'd prefer to skip that line and continue looking in other places such as the user's
* home directory.
*
* IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc()
*/
function syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder) {
const sourceNpmrcPath = path.join(sourceNpmrcFolder, '.npmrc');
const targetNpmrcPath = path.join(targetNpmrcFolder, '.npmrc');
try {
if (fs.existsSync(sourceNpmrcPath)) {
let npmrcFileLines = fs.readFileSync(sourceNpmrcPath).toString().split('\n');
npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim());
const resultLines = [];
// Trim out lines that reference environment variables that aren't defined
for (const line of npmrcFileLines) {
// This finds environment variable tokens that look like "${VAR_NAME}"
const regex = /\$\{([^\}]+)\}/g;
const environmentVariables = line.match(regex);
let lineShouldBeTrimmed = false;
if (environmentVariables) {
for (const token of environmentVariables) {
// Remove the leading "${" and the trailing "}" from the token
const environmentVariableName = token.substring(2, token.length - 1);
if (!process.env[environmentVariableName]) {
lineShouldBeTrimmed = true;
break;
}
}
}
if (lineShouldBeTrimmed) {
// Example output:
// "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}"
resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line);
}
else {
resultLines.push(line);
}
}
fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL));
}
else if (fs.existsSync(targetNpmrcPath)) {
// If the source .npmrc doesn't exist and there is one in the target, delete the one in the target
fs.unlinkSync(targetNpmrcPath);
}
}
catch (e) {
throw new Error(`Error syncing .npmrc file: ${e}`);
}
}
/**
* Detects if the package in the specified directory is installed
*/
function isPackageAlreadyInstalled(packageInstallFolder) {
try {
const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (!fs.existsSync(flagFilePath)) {
return false;
}
const fileContents = fs.readFileSync(flagFilePath).toString();
return fileContents.trim() === process.version;
}
catch (e) {
return false;
}
}
/**
* Removes the following files and directories under the specified folder path:
* - installed.flag
* -
* - node_modules
*/
function cleanInstallFolder(rushCommonFolder, packageInstallFolder) {
try {
const flagFile = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME);
if (fs.existsSync(flagFile)) {
fs.unlinkSync(flagFile);
}
const packageLockFile = path.resolve(packageInstallFolder, 'package-lock.json');
if (fs.existsSync(packageLockFile)) {
fs.unlinkSync(packageLockFile);
}
const nodeModulesFolder = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME);
if (fs.existsSync(nodeModulesFolder)) {
const rushRecyclerFolder = ensureAndJoinPath(rushCommonFolder, 'temp', 'rush-recycler', `install-run-${Date.now().toString()}`);
fs.renameSync(nodeModulesFolder, rushRecyclerFolder);
}
}
catch (e) {
throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`);
}
}
function createPackageJson(packageInstallFolder, name, version) {
try {
const packageJsonContents = {
'name': 'ci-rush',
'version': '0.0.0',
'dependencies': {
[name]: version
},
'description': 'DON\'T WARN',
'repository': 'DON\'T WARN',
'license': 'MIT'
};
const packageJsonPath = path.join(packageInstallFolder, PACKAGE_JSON_FILENAME);
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2));
}
catch (e) {
throw new Error(`Unable to create package.json: ${e}`);
}
}
/**
* Run "npm install" in the package install folder.
*/
function installPackage(packageInstallFolder, name, version) {
try {
console.log(`Installing ${name}...`);
const npmPath = getNpmPath();
const result = childProcess.spawnSync(npmPath, ['install'], {
stdio: 'inherit',
cwd: packageInstallFolder,
env: process.env
});
if (result.status !== 0) {
throw new Error('"npm install" encountered an error');
}
console.log(`Successfully installed ${name}@${version}`);
}
catch (e) {
throw new Error(`Unable to install package: ${e}`);
}
}
/**
* Get the ".bin" path for the package.
*/
function getBinPath(packageInstallFolder, binName) {
const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin');
const resolvedBinName = (os.platform() === 'win32') ? `${binName}.cmd` : binName;
return path.resolve(binFolderPath, resolvedBinName);
}
/**
* Write a flag file to the package's install directory, signifying that the install was successful.
*/
function writeFlagFile(packageInstallFolder) {
try {
const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME);
fs.writeFileSync(flagFilePath, process.version);
}
catch (e) {
throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`);
}
}
function installAndRun(packageName, packageVersion, packageBinName, packageBinArgs) {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = path.join(rushJsonFolder, 'common');
const packageInstallFolder = ensureAndJoinPath(rushCommonFolder, 'temp', 'install-run', `${packageName}@${packageVersion}`);
if (!isPackageAlreadyInstalled(packageInstallFolder)) {
// The package isn't already installed
cleanInstallFolder(rushCommonFolder, packageInstallFolder);
const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush');
syncNpmrc(sourceNpmrcFolder, packageInstallFolder);
createPackageJson(packageInstallFolder, packageName, packageVersion);
installPackage(packageInstallFolder, packageName, packageVersion);
writeFlagFile(packageInstallFolder);
}
const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`;
const statusMessageLine = new Array(statusMessage.length + 1).join('-');
console.log(os.EOL + statusMessage + os.EOL + statusMessageLine + os.EOL);
const binPath = getBinPath(packageInstallFolder, packageBinName);
const result = childProcess.spawnSync(binPath, packageBinArgs, {
stdio: 'inherit',
cwd: process.cwd(),
env: process.env
});
return result.status;
}
exports.installAndRun = installAndRun;
function runWithErrorAndStatusCode(fn) {
process.exitCode = 1;
try {
const exitCode = fn();
process.exitCode = exitCode;
}
catch (e) {
console.error(os.EOL + os.EOL + e.toString() + os.EOL + os.EOL);
}
}
exports.runWithErrorAndStatusCode = runWithErrorAndStatusCode;
function run() {
const [nodePath, /* Ex: /bin/node */ scriptPath, /* /repo/common/scripts/install-run-rush.js */ rawPackageSpecifier, /* qrcode@^1.2.0 */ packageBinName, /* qrcode */ ...packageBinArgs /* [-f, myproject/lib] */] = process.argv;
if (!nodePath) {
throw new Error('Unexpected exception: could not detect node path');
}
if (path.basename(scriptPath).toLowerCase() !== 'install-run.js') {
// If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control
// to the script that (presumably) imported this file
return;
}
if (process.argv.length < 4) {
console.log('Usage: install-run.js <package>@<version> <command> [args...]');
console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io');
process.exit(1);
}
runWithErrorAndStatusCode(() => {
const rushJsonFolder = findRushJsonFolder();
const rushCommonFolder = ensureAndJoinPath(rushJsonFolder, 'common');
const packageSpecifier = parsePackageSpecifier(rawPackageSpecifier);
const name = packageSpecifier.name;
const version = resolvePackageVersion(rushCommonFolder, packageSpecifier);
if (packageSpecifier.version !== version) {
console.log(`Resolved to ${name}@${version}`);
}
return installAndRun(name, version, packageBinName, packageBinArgs);
});
}
run();
//# sourceMappingURL=install-run.js.map

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"declaration": true,
"module": "commonjs",
"target": "es2017",
"outDir": "out",
"lib": [
"es6"
],
"moduleResolution": "node",
"sourceMap": true,
"rootDir": "../../src",
"strictNullChecks": true,
"noFallthroughCasesInSwitch": true,
"preserveWatchOutput": true,
"newLine": "lf",
"noImplicitReturns": true,
"experimentalDecorators": true
},
"include": [
"../../src/**/*.ts"
],
"exclude": [
"../../node_modules",
"../../test",
"../../**/view"
]
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "./common.tsconfig.json",
"compilerOptions": {
"declaration": false,
"strict": true
}
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "http://json.schemastore.org/tsconfig",
"extends": "./common.tsconfig.json"
}

View File

@@ -0,0 +1,18 @@
{
"name": "typescript-config",
"description": "TypeScript configurations",
"author": "GitHub",
"private": true,
"version": "0.0.1",
"publisher": "GitHub",
"repository": {
"type": "git",
"url": "https://github.com/github/vscode-codeql"
},
"scripts": {
"build": "",
"format": ""
},
"devDependencies": {},
"dependencies": {}
}

View File

@@ -0,0 +1,16 @@
.vs/**
.vscode/**
.vscode-test/**
typings/**
out/test/**
out/vscode-tests/**
**/@types/**
**/*.ts
test/**
src/**
**/*.map
.gitignore
gulpfile.js/**
tsconfig.json
tsfmt.json
vsc-extension-quickstart.md

View File

@@ -0,0 +1,98 @@
# CodeQL extension for Visual Studio Code
This project is an extension for Visual Studio Code that adds rich language support for [CodeQL](https://help.semmle.com/codeql) and allows you to easily find problems in codebases. In particular, the extension:
* Enables you to use CodeQL to query databases generated from source code.
* Shows the flow of data through the results of path queries, which is essential for triaging security results.
* Provides an easy way to run queries from the large, open source repository of [CodeQL security queries](https://github.com/Semmle/ql).
* Adds IntelliSense to support you writing and editing your own CodeQL query and library files.
## Quick start overview
The information in this `README` file describes the quickest way to start using CodeQL.
For information about other configurations, see the separate [CodeQL help](https://help.semmle.com/codeql/codeql-for-vscode.html).
**Quick start: Installing and configuring the extension**
1. [Install the extension](#installing-the-extension).
1. [Check access to the CodeQL CLI](#checking-access-to-the-codeql-cli).
1. [Clone the CodeQL starter workspace](#cloning-the-codeql-starter-workspace).
**Quick start: Using CodeQL**
1. [Import a database from LGTM.com](#importing-a-database-from-lgtmcom).
1. [Run a query](#running-a-query).
-----
## Quick start: Installing and configuring the extension
### Installing the extension
The CodeQL extension requires a minimum of Visual Studio Code 1.39. Older versions are not supported.
1. Install and open Visual Studio Code.
1. Open the Extensions view (press **Ctrl+Shift+X** or **Cmd+Shift+X**).
1. At the top of the Extensions view, type `CodeQL` in the box labeled **Search Extensions in Marketplace**.
1. Locate the CodeQL extension and select **Install**. This will install the extension from the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=github.vscode-codeql).
### Checking access to the CodeQL CLI
The extension uses the [CodeQL CLI](https://help.semmle.com/codeql/codeql-cli.html) to compile and run queries. The extension automatically manages access to the CLI for you by default (recommended). To check for updates to the CodeQL CLI, you can use the **CodeQL: Check for CLI Updates** command.
If you want to override the default behavior and use a CodeQL CLI that's already on your machine, see [Configuring access to the CodeQL CLI](https://help.semmle.com/codeql/codeql-for-vscode/procedures/setting-up.html#configuring-access-to-the-codeql-cli).
If you have any difficulty with CodeQL CLI access, see the **CodeQL Extension Log** in the **Output** view for any error messages.
### Cloning the CodeQL starter workspace
When you're working with CodeQL, you need access to the standard CodeQL libraries and queries.
Initially, we recommend that you clone and use the ready-to-use starter workspace, https://github.com/github/vscode-codeql-starter/.
This includes libraries and queries for the main supported languages, with folders set up ready for your custom queries. After cloning the workspace (use `git clone --recursive`), you can use it in the same way as any other VS Code workspace—with the added advantage that you can easily update the CodeQL libraries.
For information about configuring an existing workspace for CodeQL, [see the documentation](https://help.semmle.com/codeql/codeql-for-vscode/procedures/setting-up.html#updating-an-existing-workspace-for-codeql).
## Quick start: Using CodeQL
You can find all the commands contributed by the extension in the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**) by typing `CodeQL`, many of them are also accessible through the interface, and via keyboard shortcuts.
### Importing a database from LGTM.com
While you can use the [CodeQL CLI to create your own databases](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html), the simplest way to start is by downloading a database from LGTM.com.
1. Log in to LGTM.com.
1. Find a project you're interested in and display the **Integrations** tab (for example, [Apache Kafka](https://lgtm.com/projects/g/apache/kafka/ci/)).
1. Scroll to the **CodeQL databases for local analysis** section at the bottom of the page.
1. Download databases for the languages that you want to explore.
1. Unzip the databases.
1. For each database that you want to import:
1. In the VS Code sidebar, go to **CodeQL** > **Databases** and click **+**.
1. Browse to the unzipped database folder (the parent folder that contains `db-<language>` and `src`) and select **Choose database** to add it.
When the import is complete, each CodeQL database is displayed in the CodeQL sidebar under **Databases**.
### Running a query
The instructions below assume that you're using the CodeQL starter workspace, or that you've added the CodeQL libraries and queries repository to your workspace.
1. Expand the `ql` folder and locate a query to run. The standard queries are grouped by target language and then type, for example: `ql/java/ql/src/Likely Bugs`.
1. Open a query (`.ql`) file.
3. Right-click in the query window and select **CodeQL: Run Query**. Alternatively, open the Command Palette (**Ctrl+Shift+P** or **Cmd+Shift+P**), type `Run Query`, then select **CodeQL: Run Query**.
The CodeQL extension runs the query on the current database using the CLI and reports progress in the bottom right corner of the application.
When the results are ready, they're displayed in the CodeQL Query Results view. Use the dropdown menu to choose between different forms of result output.
If there are any problems running a query, a notification is displayed in the bottom right corner of the application. In addition to the error message, the notification includes details of how to fix the problem.
## What next?
For more information about the CodeQL extension, [see the documentation](https://help.semmle.com/codeql/codeql-for-vscode.html). Otherwise, you could:
* [Create a database for a different codebase](https://help.semmle.com/codeql/codeql-cli/procedures/create-codeql-database.html).
* [Try out variant analysis](https://help.semmle.com/QL/learn-ql/ql-training.html).
* [Learn more about CodeQL](https://help.semmle.com/QL/learn-ql/).
* [Read how security researchers use CodeQL to find CVEs](https://securitylab.github.com/research).
## License
The CodeQL extension for Visual Studio Code is [licensed](LICENSE.md) under the MIT License. The version of CodeQL used by the CodeQL extension is subject to the [GitHub CodeQL Terms & Conditions](https://securitylab.github.com/tools/codeql/license).

View File

@@ -0,0 +1,19 @@
'use strict';
require('ts-node').register({});
const gulp = require('gulp');
const {
compileTypeScript,
watchTypeScript,
packageExtension,
compileTextMateGrammar,
copyTestData,
copyViewCss
} = require('build-tasks');
const { compileView } = require('./webpack');
exports.buildWithoutPackage = gulp.parallel(compileTypeScript, compileTextMateGrammar, compileView, copyTestData, copyViewCss);
exports.compileTextMateGrammar = compileTextMateGrammar;
exports.default = gulp.series(exports.buildWithoutPackage, packageExtension);
exports.watchTypeScript = watchTypeScript;
exports.compileTypeScript = compileTypeScript;

View File

@@ -0,0 +1,61 @@
import * as path from 'path';
import * as webpack from 'webpack';
export const config: webpack.Configuration = {
mode: 'development',
entry: {
resultsView: './src/view/results.tsx'
},
output: {
path: path.resolve(__dirname, '..', 'out'),
filename: "[name].js"
},
devtool: 'source-map',
resolve: {
extensions: ['.js', '.ts', '.tsx', '.json']
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: 'ts-loader',
},
{
test: /\.less$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
sourceMap: true
}
},
{
loader: 'less-loader',
options: {
javascriptEnabled: true,
sourceMap: true
}
}
]
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader'
}
]
}
]
},
performance: {
hints: false
}
};

View File

@@ -0,0 +1,27 @@
import * as webpack from 'webpack';
import { config } from './webpack.config';
export function compileView(cb: (err?: Error) => void) {
webpack(config).run((error, stats) => {
if (error) {
cb(error);
}
console.log(stats.toString({
errorDetails: true,
colors: true,
assets: false,
builtAt: false,
version: false,
hash: false,
entrypoints: false,
timings: false,
modules: false
}));
if (stats.hasErrors()) {
cb(new Error('Compilation errors detected.'));
return;
}
cb();
});
}

View File

@@ -0,0 +1,72 @@
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "//",
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
"blockComment": [
"/*",
"*/"
]
},
// symbols used as brackets
"brackets": [
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
]
],
// symbols that are auto closed when typing
"autoClosingPairs": [
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
],
[
"\"",
"\""
],
[
"'",
"'"
]
],
// symbols that that can be used to surround a selection
"surroundingPairs": [
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
],
[
"\"",
"\""
],
[
"'",
"'"
]
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30px"
width="30px"
fill="#000000"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
style="enable-background:new 0 0 100 100;"
xml:space="preserve"
id="svg3895"
sodipodi:docname="black-plus.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata3901"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3899" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1179"
inkscape:window-height="701"
id="namedview3897"
showgrid="false"
inkscape:zoom="7.8666667"
inkscape:cx="8.516949"
inkscape:cy="15"
inkscape:window-x="33"
inkscape:window-y="56"
inkscape:window-maximized="0"
inkscape:current-layer="svg3895" /><rect
style="opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.24023867;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect29"
width="15.499873"
height="88.381233"
x="-58.597389"
y="5.8093848"
transform="scale(-1,1)" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.24023867;stroke-opacity:1"
id="rect29-6"
width="15.499873"
height="88.381233"
x="42.250065"
y="-95.038071"
transform="rotate(90)" /></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30px"
width="30px"
fill="#000000"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
style="enable-background:new 0 0 100 100;"
xml:space="preserve"
id="svg3895"
sodipodi:docname="check-dark-mode.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata3901"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3899" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1179"
inkscape:window-height="701"
id="namedview3897"
showgrid="false"
inkscape:zoom="7.8666667"
inkscape:cx="-22.881356"
inkscape:cy="15"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="0"
inkscape:current-layer="svg3895" /><g
transform="translate(-452.57627,-74.457627)"
id="g3893"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"><g
id="g3891"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"><g
id="g3889"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"><path
d="M 457.98136,131.82373 488.80508,163.49492 548.8661,92.416949 535.93729,81.60678 487.85763,144.41695 468.2678,120.58983 Z"
id="path3887"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1" /></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30px"
width="30px"
fill="#000000"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
style="enable-background:new 0 0 100 100;"
xml:space="preserve"
id="svg3895"
sodipodi:docname="check.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata3901"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3899" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1179"
inkscape:window-height="701"
id="namedview3897"
showgrid="false"
inkscape:zoom="7.8666667"
inkscape:cx="15"
inkscape:cy="15"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="0"
inkscape:current-layer="svg3895" /><g
transform="translate(-452.57627,-74.457627)"
id="g3893"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"><g
id="g3891"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"><g
id="g3889"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-opacity:1"><path
d="M 457.98136,131.82373 488.80508,163.49492 548.8661,92.416949 535.93729,81.60678 487.85763,144.41695 468.2678,120.58983 Z"
id="path3887"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc"
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-opacity:1" /></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="27px" height="16px" viewBox="0 0 27 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 59 (86127) - https://sketch.com -->
<title>Slice</title>
<desc>Created with Sketch.</desc>
<g id="light" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="QL" transform="translate(1.000000, 1.000000)">
<rect id="Rectangle-41" stroke="#2088FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x="0" y="0" width="25" height="14" rx="2"></rect>
<line x1="17" y1="5" x2="19" y2="5" id="Stroke-15" stroke="#2088FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></line>
<line x1="17" y1="9" x2="21" y2="9" id="Stroke-15" stroke="#2088FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></line>
<path d="M8.85227273,7 C8.85227273,7.51894199 8.76988719,7.97537682 8.60511364,8.36931818 C8.44034009,8.76325955 8.21591051,9.08711994 7.93181818,9.34090909 L8.76420455,10.3863636 L7.61647727,10.3863636 L7.14772727,9.80965909 C6.83143781,9.92897787 6.49147909,9.98863636 6.12784091,9.98863636 C5.61079287,9.98863636 5.14678236,9.8712133 4.73579545,9.63636364 C4.32480855,9.40151398 4.00000119,9.06108178 3.76136364,8.61505682 C3.52272608,8.16903186 3.40340909,7.63068497 3.40340909,7 C3.40340909,6.36552713 3.52272608,5.8257598 3.76136364,5.38068182 C4.00000119,4.93560384 4.32480855,4.59611859 4.73579545,4.36221591 C5.14678236,4.12831322 5.61079287,4.01136364 6.12784091,4.01136364 C6.642995,4.01136364 7.10605855,4.12831322 7.51704545,4.36221591 C7.92803236,4.59611859 8.2533132,4.93560384 8.49289773,5.38068182 C8.73248226,5.8257598 8.85227273,6.36552713 8.85227273,7 Z M5.70170455,7.88636364 L6.74715909,7.88636364 L7.17897727,8.44034091 C7.31344764,8.27935526 7.41808675,8.07859969 7.49289773,7.83806818 C7.56770871,7.59753668 7.60511364,7.31818341 7.60511364,7 C7.60511364,6.38257267 7.47064528,5.91145996 7.20170455,5.58664773 C6.93276381,5.2618355 6.57481284,5.09943182 6.12784091,5.09943182 C5.68086898,5.09943182 5.32291801,5.2618355 5.05397727,5.58664773 C4.78503653,5.91145996 4.65056818,6.38257267 4.65056818,7 C4.65056818,7.61553338 4.78503653,8.08617261 5.05397727,8.41193182 C5.32291801,8.73769102 5.68086898,8.90056818 6.12784091,8.90056818 C6.23958389,8.90056818 6.34564344,8.89015162 6.44602273,8.86931818 L5.70170455,7.88636364 Z M10.1813315,10 L10.1813315,4 L11.4114451,4 L11.4114451,8.98579545 L13.9057633,8.98579545 L13.9057633,10 L10.1813315,10 Z" fill="#2088FF" fill-rule="nonzero"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30px"
width="30px"
fill="#000000"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
style="enable-background:new 0 0 100 100;"
xml:space="preserve"
id="svg3895"
sodipodi:docname="red-x.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata3901"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3899" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1179"
inkscape:window-height="701"
id="namedview3897"
showgrid="false"
inkscape:zoom="7.8666667"
inkscape:cx="-22.881356"
inkscape:cy="15"
inkscape:window-x="33"
inkscape:window-y="36"
inkscape:window-maximized="0"
inkscape:current-layer="svg3895" /><rect
style="opacity:1;vector-effect:none;fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:2.24023867;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect29"
width="15.499873"
height="88.381233"
x="-8.3491764"
y="27.119303"
transform="matrix(-0.70710678,0.70710678,0.70710678,0.70710678,0,0)" /><rect
style="fill:#ff0000;fill-opacity:1;stroke:none;stroke-width:2.24023867;stroke-opacity:1"
id="rect29-6"
width="15.499873"
height="88.381233"
x="63.559982"
y="-44.789856"
transform="rotate(45)" /></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="30px"
width="30px"
fill="#000000"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 100 100"
style="enable-background:new 0 0 100 100;"
xml:space="preserve"
id="svg3895"
sodipodi:docname="white-plus.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)"><metadata
id="metadata3901"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3899" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1179"
inkscape:window-height="701"
id="namedview3897"
showgrid="false"
inkscape:zoom="7.8666667"
inkscape:cx="8.516949"
inkscape:cy="15"
inkscape:window-x="33"
inkscape:window-y="56"
inkscape:window-maximized="0"
inkscape:current-layer="svg3895" /><rect
style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.24023867;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="rect29"
width="15.499873"
height="88.381233"
x="-58.597389"
y="5.8093848"
transform="scale(-1,1)" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.24023867;stroke-opacity:1"
id="rect29-6"
width="15.499873"
height="88.381233"
x="42.250065"
y="-95.038071"
transform="rotate(90)" /></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,344 @@
{
"name": "vscode-codeql",
"displayName": "CodeQL",
"description": "CodeQL for Visual Studio Code",
"author": "GitHub",
"private": true,
"version": "1.0.0",
"publisher": "GitHub",
"license": "MIT",
"icon": "media/VS-marketplace-CodeQL-icon.png",
"repository": {
"type": "git",
"url": "https://github.com/github/vscode-codeql"
},
"engines": {
"vscode": "^1.39.0"
},
"categories": [
"Programming Languages"
],
"activationEvents": [
"onLanguage:ql",
"onView:codeQLDatabases",
"onView:codeQLQueryHistory",
"onCommand:codeQL.checkForUpdatesToCLI",
"onCommand:codeQL.chooseDatabase",
"onCommand:codeQL.setCurrentDatabase",
"onCommand:codeQLDatabases.chooseDatabase",
"onCommand:codeQLDatabases.setCurrentDatabase",
"onWebviewPanel:resultsView",
"onFileSystem:codeql-zip-archive"
],
"main": "./out/extension",
"files": [
"gen/*.js",
"media/**",
"out/**",
"package.json",
"language-configuration.json"
],
"contributes": {
"languages": [
{
"id": "ql",
"aliases": [
"QL",
"ql",
"CodeQL"
],
"extensions": [
".ql",
".qll"
],
"configuration": "./language-configuration.json"
},
{
"id": "dbscheme",
"aliases": [
"DBScheme",
"dbscheme"
],
"extensions": [
".dbscheme"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "ql",
"scopeName": "source.ql",
"path": "./out/syntaxes/ql.tmLanguage.json"
},
{
"language": "dbscheme",
"scopeName": "source.dbscheme",
"path": "./out/syntaxes/dbscheme.tmLanguage.json"
}
],
"configuration": {
"type": "object",
"title": "CodeQL",
"properties": {
"codeQL.cli.executablePath": {
"scope": "window",
"type": "string",
"default": "",
"description": "Path to the CodeQL executable that should be used by the CodeQL extension. The executable is named `codeql` on Linux/Mac and `codeql.cmd` on Windows. This overrides all other CodeQL CLI settings."
},
"codeQL.runningQueries.numberOfThreads": {
"type": "integer",
"default": 1,
"minimum": 1,
"maximum": 1024,
"description": "Number of threads for running queries."
},
"codeQL.runningQueries.timeout": {
"type": ["integer", "null"],
"default": null,
"minimum": 0,
"maximum": 2147483647,
"description": "Timeout (in seconds) for running queries. Leave blank or set to zero for no timeout."
},
"codeQL.runningQueries.memory": {
"type": ["integer", "null"],
"default": null,
"minimum": 1024,
"description": "Memory (in MB) to use for running queries. Leave blank for CodeQL to choose a suitable value based on your system's available memory."
},
"codeQL.runningQueries.debug": {
"type": "boolean",
"default": false,
"description": "Enable debug logging and tuple counting when running CodeQL queries. This information is useful for debugging query performance."
}
}
},
"commands": [
{
"command": "codeQL.runQuery",
"title": "CodeQL: Run Query"
},
{
"command": "codeQL.quickEval",
"title": "CodeQL: Quick Evaluation"
},
{
"command": "codeQL.chooseDatabase",
"title": "CodeQL: Choose Database",
"icon": {
"light": "media/black-plus.svg",
"dark": "media/white-plus.svg"
}
},
{
"command": "codeQL.setCurrentDatabase",
"title": "CodeQL: Set Current Database"
},
{
"command": "codeQL.upgradeCurrentDatabase",
"title": "CodeQL: Upgrade Current Database"
},
{
"command": "codeQL.clearCache",
"title": "CodeQL: Clear Cache"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"title": "Set Current Database"
},
{
"command": "codeQLDatabases.removeDatabase",
"title": "Remove Database"
},
{
"command": "codeQLDatabases.upgradeDatabase",
"title": "Upgrade Database"
},
{
"command": "codeQL.checkForUpdatesToCLI",
"title": "CodeQL: Check for CLI Updates"
},
{
"command": "codeQLQueryHistory.openQuery",
"title": "CodeQL: Open Query"
},
{
"command": "codeQLQueryHistory.itemClicked",
"title": "Query History Item"
}
],
"menus": {
"view/title": [
{
"command": "codeQL.chooseDatabase",
"when": "view == codeQLDatabases",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "codeQLDatabases.setCurrentDatabase",
"group": "inline",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.removeDatabase",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLDatabases.upgradeDatabase",
"group": "9_qlCommands",
"when": "view == codeQLDatabases"
},
{
"command": "codeQLQueryHistory.openQuery",
"group": "9_qlCommands",
"when": "view == codeQLQueryHistory"
}
],
"explorer/context": [
{
"command": "codeQL.setCurrentDatabase",
"group": "9_qlCommands",
"when": "resourceScheme == codeql-zip-archive || explorerResourceIsFolder"
},
{
"command": "codeQL.runQuery",
"group": "9_qlCommands",
"when": "resourceLangId == ql && resourceExtname == .ql"
}
],
"commandPalette": [
{
"command": "codeQL.runQuery",
"when": "resourceLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
},
{
"command": "codeQL.setCurrentDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.setCurrentDatabase",
"when": "false"
},
{
"command": "codeQLDatabases.removeDatabase",
"when": "false"
},
{
"command": "codeQLQueryHistory.openQuery",
"when": "false"
},
{
"command": "codeQLQueryHistory.itemClicked",
"when": "false"
}
],
"editor/context": [
{
"command": "codeQL.runQuery",
"when": "editorLangId == ql && resourceExtname == .ql"
},
{
"command": "codeQL.quickEval",
"when": "editorLangId == ql"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "ql-container",
"title": "CodeQL",
"icon": "media/logo.svg"
}
]
},
"views": {
"ql-container": [
{
"id": "codeQLDatabases",
"name": "Databases"
},
{
"id": "codeQLQueryHistory",
"name": "Query History"
}
]
}
},
"scripts": {
"build": "gulp",
"watch": "npm-run-all -p watch:*",
"watch:extension": "tsc --watch",
"test": "mocha --exit -r ts-node/register test/pure-tests/**/*.ts",
"preintegration": "rm -rf ./out/vscode-tests && gulp",
"integration": "node ./out/vscode-tests/run-integration-tests.js",
"update-vscode": "node ./node_modules/vscode/bin/install",
"postinstall": "node ./node_modules/vscode/bin/install",
"format": "tsfmt -r"
},
"dependencies": {
"classnames": "~2.2.6",
"fs-extra": "^8.1.0",
"glob-promise": "^3.4.0",
"node-fetch": "~2.6.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"semmle-bqrs": "^0.0.1",
"semmle-io-node": "^0.0.1",
"semmle-vscode-utils": "^0.0.1",
"tmp": "^0.1.0",
"unzipper": "~0.10.5",
"vscode-jsonrpc": "^4.0.0",
"vscode-languageclient": "^5.2.1"
},
"devDependencies": {
"@types/chai": "^4.1.7",
"@types/classnames": "~2.2.9",
"@types/fs-extra": "^8.0.0",
"@types/glob": "^7.1.1",
"@types/google-protobuf": "^3.2.7",
"@types/gulp": "^4.0.6",
"@types/jszip": "~3.1.6",
"@types/mocha": "~5.2.7",
"@types/node": "^12.0.8",
"@types/node-fetch": "~2.5.2",
"@types/react": "^16.8.17",
"@types/react-dom": "^16.8.4",
"@types/sarif": "~2.1.2",
"@types/tmp": "^0.1.0",
"@types/unzipper": "~0.10.0",
"@types/vscode": "^1.39.0",
"@types/webpack": "^4.32.1",
"@types/xml2js": "~0.4.4",
"build-tasks": "^0.0.1",
"chai": "^4.2.0",
"child-process-promise": "^2.2.1",
"css-loader": "~3.1.0",
"glob": "^7.1.4",
"gulp": "^4.0.2",
"gulp-sourcemaps": "^2.6.5",
"gulp-typescript": "^5.0.1",
"mocha": "~6.2.1",
"npm-run-all": "^4.1.5",
"style-loader": "~0.23.1",
"through2": "^3.0.1",
"ts-loader": "^5.4.5",
"ts-node": "^8.3.0",
"ts-protoc-gen": "^0.9.0",
"typescript": "^3.5.2",
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2",
"vsce": "^1.65.0",
"vscode-test": "^1.0.0",
"webpack": "^4.38.0",
"webpack-cli": "^3.3.2"
}
}

View File

@@ -0,0 +1,308 @@
import * as fs from 'fs-extra';
import * as unzipper from 'unzipper';
import * as vscode from 'vscode';
import { logger } from './logging';
// All path operations in this file must be on paths *within* the zip
// archive.
import * as _path from 'path';
const path = _path.posix;
export class File implements vscode.FileStat {
type: vscode.FileType;
ctime: number;
mtime: number;
size: number;
constructor(public name: string, public data: Uint8Array) {
this.type = vscode.FileType.File;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = data.length;
this.name = name;
}
}
export class Directory implements vscode.FileStat {
type: vscode.FileType;
ctime: number;
mtime: number;
size: number;
entries: Map<string, Entry> = new Map();
constructor(public name: string) {
this.type = vscode.FileType.Directory;
this.ctime = Date.now();
this.mtime = Date.now();
this.size = 0;
}
}
export type Entry = File | Directory;
/**
* A map containing directory hierarchy information in a convenient form.
*
* For example, if dirMap : DirectoryHierarchyMap, and /foo/bar/baz.c is a file in the
* directory structure being represented, then
*
* dirMap['/foo'] = {'bar': vscode.FileType.Directory}
* dirMap['/foo/bar'] = {'baz': vscode.FileType.File}
*/
export type DirectoryHierarchyMap = Map<string, Map<string, vscode.FileType>>;
export type ZipFileReference = { sourceArchiveZipPath: string, pathWithinSourceArchive: string };
/** Encodes a reference to a source file within a zipped source archive into a single URI. */
export function encodeSourceArchiveUri(ref: ZipFileReference): vscode.Uri {
const { sourceArchiveZipPath, pathWithinSourceArchive } = ref;
// These two paths are put into a single URI with a custom scheme.
// The path and authority components of the URI encode the two paths.
// The path component of the URI contains both paths, joined by a slash.
let encodedPath = path.join(sourceArchiveZipPath, pathWithinSourceArchive);
// If a URI contains an authority component, then the path component
// must either be empty or begin with a slash ("/") character.
// (Source: https://tools.ietf.org/html/rfc3986#section-3.3)
// Since we will use an authority component, we add a leading slash if necessary
// (paths on Windows usually start with the drive letter).
let sourceArchiveZipPathStartIndex: number;
if (encodedPath.startsWith('/')) {
sourceArchiveZipPathStartIndex = 0;
} else {
encodedPath = '/' + encodedPath;
sourceArchiveZipPathStartIndex = 1;
}
// The authority component of the URI records the 0-based inclusive start and exclusive end index
// of the source archive zip path within the path component of the resulting URI.
// This lets us separate the paths, ignoring the leading slash if we added one.
const sourceArchiveZipPathEndIndex = sourceArchiveZipPathStartIndex + sourceArchiveZipPath.length;
const authority = `${sourceArchiveZipPathStartIndex}-${sourceArchiveZipPathEndIndex}`;
return vscode.Uri.parse(zipArchiveScheme + ':/').with({
path: encodedPath,
authority,
});
}
const sourceArchiveUriAuthorityPattern = /^(\d+)\-(\d+)$/;
class InvalidSourceArchiveUriError extends Error {
constructor(uri: vscode.Uri) {
super(`Can't decode uri ${uri}: authority should be of the form startIndex-endIndex (where both indices are integers).`);
}
}
/** Decodes an encoded source archive URI into its corresponding paths. Inverse of `encodeSourceArchiveUri`. */
export function decodeSourceArchiveUri(uri: vscode.Uri): ZipFileReference {
const match = sourceArchiveUriAuthorityPattern.exec(uri.authority);
if (match === null)
throw new InvalidSourceArchiveUriError(uri);
const zipPathStartIndex = parseInt(match[1]);
const zipPathEndIndex = parseInt(match[2]);
if (isNaN(zipPathStartIndex) || isNaN(zipPathEndIndex))
throw new InvalidSourceArchiveUriError(uri);
return {
pathWithinSourceArchive: uri.path.substring(zipPathEndIndex),
sourceArchiveZipPath: uri.path.substring(zipPathStartIndex, zipPathEndIndex),
};
}
/**
* Make sure `file` and all of its parent directories are represented in `map`.
*/
function ensureFile(map: DirectoryHierarchyMap, file: string) {
const dirname = path.dirname(file);
if (dirname === '.') {
const error = `Ill-formed path ${file} in zip archive (expected absolute path)`;
logger.log(error);
throw new Error(error);
}
ensureDir(map, dirname);
map.get(dirname)!.set(path.basename(file), vscode.FileType.File);
}
/**
* Make sure `dir` and all of its parent directories are represented in `map`.
*/
function ensureDir(map: DirectoryHierarchyMap, dir: string) {
const parent = path.dirname(dir);
if (!map.has(dir)) {
map.set(dir, new Map);
if (dir !== parent) { // not the root directory
ensureDir(map, parent);
map.get(parent)!.set(path.basename(dir), vscode.FileType.Directory);
}
}
}
type Archive = {
unzipped: unzipper.CentralDirectory,
dirMap: DirectoryHierarchyMap,
};
export class ArchiveFileSystemProvider implements vscode.FileSystemProvider {
private readOnlyError = vscode.FileSystemError.NoPermissions('write operation attempted, but source archive filesystem is readonly');
private archives: Map<string, Archive> = new Map;
private async getArchive(zipPath: string): Promise<Archive> {
if (!this.archives.has(zipPath)) {
if (!await fs.pathExists(zipPath))
throw vscode.FileSystemError.FileNotFound(zipPath);
const archive: Archive = { unzipped: await unzipper.Open.file(zipPath), dirMap: new Map };
archive.unzipped.files.forEach(f => { ensureFile(archive.dirMap, path.resolve('/', f.path)); });
this.archives.set(zipPath, archive);
}
return this.archives.get(zipPath)!;
}
root = new Directory('');
// metadata
async stat(uri: vscode.Uri): Promise<vscode.FileStat> {
return await this._lookup(uri, false);
}
async readDirectory(uri: vscode.Uri): Promise<[string, vscode.FileType][]> {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
let contents = archive.dirMap.get(ref.pathWithinSourceArchive);
const result = contents === undefined ? [] : Array.from(contents.entries());
if (result === undefined) {
throw vscode.FileSystemError.FileNotFound(uri);
}
return result;
}
// file contents
async readFile(uri: vscode.Uri): Promise<Uint8Array> {
const data = (await this._lookupAsFile(uri, false)).data;
if (data) {
return data;
}
throw vscode.FileSystemError.FileNotFound();
}
// write operations, all disabled
writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void {
throw this.readOnlyError;
}
rename(oldUri: vscode.Uri, newUri: vscode.Uri, options: { overwrite: boolean }): void {
throw this.readOnlyError;
}
delete(uri: vscode.Uri): void {
throw this.readOnlyError;
}
createDirectory(uri: vscode.Uri): void {
throw this.readOnlyError;
}
// content lookup
private async _lookup(uri: vscode.Uri, silent: boolean): Promise<Entry> {
const ref = decodeSourceArchiveUri(uri);
const archive = await this.getArchive(ref.sourceArchiveZipPath);
// this is a path inside the archive, so don't use `.fsPath`, and
// use '/' as path separator throughout
const reqPath = ref.pathWithinSourceArchive;
const file = archive.unzipped.files.find(
f => {
const absolutePath = path.resolve('/', f.path);
return absolutePath === reqPath
|| absolutePath === path.join('/src_archive', reqPath);
}
);
if (file !== undefined) {
if (file.type === 'File') {
return new File(reqPath, await file.buffer());
}
else { // file.type === 'Directory'
// I haven't observed this case in practice. Could it happen
// with a zip file that contains empty directories?
return new Directory(reqPath);
}
}
if (archive.dirMap.has(reqPath)) {
return new Directory(reqPath);
}
throw vscode.FileSystemError.FileNotFound(uri);
}
private async _lookupAsDirectory(uri: vscode.Uri, silent: boolean): Promise<Directory> {
let entry = await this._lookup(uri, silent);
if (entry instanceof Directory) {
return entry;
}
throw vscode.FileSystemError.FileNotADirectory(uri);
}
private async _lookupAsFile(uri: vscode.Uri, silent: boolean): Promise<File> {
let entry = await this._lookup(uri, silent);
if (entry instanceof File) {
return entry;
}
throw vscode.FileSystemError.FileIsADirectory(uri);
}
private _lookupParentDirectory(uri: vscode.Uri): Promise<Directory> {
const dirname = uri.with({ path: path.dirname(uri.path) });
return this._lookupAsDirectory(dirname, false);
}
// file events
private _emitter = new vscode.EventEmitter<vscode.FileChangeEvent[]>();
private _bufferedEvents: vscode.FileChangeEvent[] = [];
private _fireSoonHandle?: NodeJS.Timer;
readonly onDidChangeFile: vscode.Event<vscode.FileChangeEvent[]> = this._emitter.event;
watch(_resource: vscode.Uri): vscode.Disposable {
// ignore, fires for all changes...
return new vscode.Disposable(() => { });
}
private _fireSoon(...events: vscode.FileChangeEvent[]): void {
this._bufferedEvents.push(...events);
if (this._fireSoonHandle) {
clearTimeout(this._fireSoonHandle);
}
this._fireSoonHandle = setTimeout(() => {
this._emitter.fire(this._bufferedEvents);
this._bufferedEvents.length = 0;
}, 5);
}
}
/**
* Custom uri scheme for referring to files inside zip archives stored
* in the filesystem. See `encodeSourceArchiveUri`/`decodeSourceArchiveUri` for
* how these uris are constructed.
*
* (cf. https://www.ietf.org/rfc/rfc2396.txt (Appendix A, page 26) for
* the fact that hyphens are allowed in uri schemes)
*/
export const zipArchiveScheme = 'codeql-zip-archive';
export function activate(ctx: vscode.ExtensionContext) {
ctx.subscriptions.push(vscode.workspace.registerFileSystemProvider(
zipArchiveScheme,
new ArchiveFileSystemProvider(),
{
isCaseSensitive: true,
isReadonly: true,
}
));
}

11
extensions/ql-vscode/src/blob.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/**
* The npm library jszip is designed to work in both the browser and
* node. Consequently its typings @types/jszip refers to both node
* types like `Buffer` (which don't exist in the browser), and browser
* types like `Blob` (which don't exist in node). Instead of sticking
* all of `dom` in `compilerOptions.lib`, it suffices just to put in a
* stub definition of the type `Blob` here so that compilation
* succeeds.
*/
declare type Blob = string;

View File

@@ -0,0 +1,96 @@
import { runCodeQlCliCommand } from "./cli";
import { Logger } from "./logging";
/**
* Get the version of a CodeQL CLI.
*/
export async function getCodeQlCliVersion(codeQlPath: string, logger: Logger): Promise<Version | undefined> {
const output: string = await runCodeQlCliCommand(
codeQlPath,
["version"],
["--format=terse"],
"Checking CodeQL version",
logger
);
return tryParseVersionString(output.trim());
}
/**
* Try to parse a version string, returning undefined if we can't parse it.
*
* Version strings must contain a major, minor, and patch version. They may optionally
* start with "v" and may optionally contain some "tail" string after the major, minor, and
* patch versions, for example as in `v2.1.0+baf5bff`.
*/
export function tryParseVersionString(versionString: string): Version | undefined {
const match = versionString.match(versionRegex);
if (match === null) {
return undefined;
}
return {
buildMetadata: match[5],
majorVersion: Number.parseInt(match[1], 10),
minorVersion: Number.parseInt(match[2], 10),
patchVersion: Number.parseInt(match[3], 10),
prereleaseVersion: match[4],
rawString: versionString,
}
}
/**
* Regex for parsing semantic versions
*
* From the semver spec https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
*/
const versionRegex = new RegExp(String.raw`^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)` +
String.raw`(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?` +
String.raw`(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`);
/**
* A version of the CodeQL CLI.
*/
export interface Version {
/**
* Build metadata
*
* For example, this will be `abcdef0` for version 2.1.0-alpha.1+abcdef0.
* Build metadata must be ignored when comparing versions.
*/
buildMetadata: string | undefined;
/**
* Major version number
*
* For example, this will be `2` for version 2.1.0-alpha.1+abcdef0.
*/
majorVersion: number;
/**
* Minor version number
*
* For example, this will be `1` for version 2.1.0-alpha.1+abcdef0.
*/
minorVersion: number;
/**
* Patch version number
*
* For example, this will be `0` for version 2.1.0-alpha.1+abcdef0.
*/
patchVersion: number;
/**
* Prerelease version
*
* For example, this will be `alpha.1` for version 2.1.0-alpha.1+abcdef0.
* The prerelease version must be considered when comparing versions.
*/
prereleaseVersion: string | undefined;
/**
* Raw version string
*
* For example, this will be `2.1.0-alpha.1+abcdef0` for version 2.1.0-alpha.1+abcdef0.
*/
rawString: string;
}

View File

@@ -0,0 +1,496 @@
import * as child_process from "child_process";
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sarif from 'sarif';
import * as util from 'util';
import { Logger, ProgressReporter } from "./logging";
import { Disposable } from "vscode";
import { DistributionProvider } from "./distribution";
import { SortDirection } from "./interface-types";
import { assertNever } from "./helpers-pure";
/**
* The version of the SARIF format that we are using.
*/
const SARIF_FORMAT = "sarifv2.1.0";
/**
* Flags to pass to all cli commands.
*/
const LOGGING_FLAGS = ['-v', '--log-to-stderr'];
/**
* The expected output of `codeql resolve library-path`.
*/
export interface QuerySetup {
libraryPath: string[],
dbscheme: string,
relativeName?: string,
compilationCache?: string
}
/**
* The expected output of `codeql resolve database`.
*/
export interface DbInfo {
sourceLocationPrefix: string;
columnKind: string;
unicodeNewlines: boolean;
sourceArchiveZip: string;
sourceArchiveRoot: string;
datasetFolder: string;
logsFolder: string;
}
/**
* The expected output of `codeql resolve upgrades`.
*/
export interface UpgradesInfo {
scripts: string[];
finalDbscheme: string;
}
/**
* The expected output of `codeql resolve metadata`.
*/
export interface QueryMetadata {
name?: string,
description?: string,
id?: string,
kind?: string
}
// `codeql bqrs interpret` requires both of these to be present or
// both absent.
export interface SourceInfo {
sourceArchive: string;
sourceLocationPrefix: string;
}
/**
* This class manages a cli server started by `codeql execute cli-server` to
* run commands without the overhead of starting a new java
* virtual machine each time. This class also controls access to the server
* by queueing the commands sent to it.
*/
export class CodeQLCliServer implements Disposable {
/** The process for the cli server, or undefined if one doesn't exist yet */
process?: child_process.ChildProcessWithoutNullStreams;
/** Queue of future commands*/
commandQueue: (() => void)[];
/** Whether a command is running */
commandInProcess: boolean;
/** A buffer with a single null byte. */
nullBuffer: Buffer;
constructor(private config: DistributionProvider, private logger: Logger) {
this.commandQueue = [];
this.commandInProcess = false;
this.nullBuffer = Buffer.alloc(1);
if (this.config.onDidChangeDistribution) {
this.config.onDidChangeDistribution(() => {
this.restartCliServer();
});
}
}
dispose() {
this.killProcessIfRunning();
}
killProcessIfRunning() {
if (this.process) {
// Tell the Java CLI server process to shut down.
this.logger.log('Sending shutdown request');
try {
this.process.stdin.write(JSON.stringify(["shutdown"]), "utf8");
this.process.stdin.write(this.nullBuffer);
this.logger.log('Sent shutdown request');
} catch (e) {
// We are probably fine here, the process has already closed stdin.
this.logger.log(`Shutdown request failed: process stdin may have already closed. The error was ${e}`);
this.logger.log('Stopping the process anyway.');
}
// Close the stdin and stdout streams.
// This is important on Windows where the child process may not die cleanly.
this.process.stdin.end();
this.process.kill();
this.process.stdout.destroy();
this.process.stderr.destroy();
this.process = undefined;
}
}
/**
* Restart the server when the current command terminates
*/
private restartCliServer() {
let callback = () => {
try {
this.killProcessIfRunning();
} finally {
this.runNext();
}
};
// If the server is not running a command run this immediately
// otherwise add to the front of the queue (as we want to run this after the next command()).
if (this.commandInProcess) {
this.commandQueue.unshift(callback)
} else {
callback();
}
}
/**
* Launch the cli server
*/
private async launchProcess(): Promise<child_process.ChildProcessWithoutNullStreams> {
const config = await this.config.getCodeQlPathWithoutVersionCheck();
if (!config) {
throw new Error("Failed to find codeql distribution")
}
return spawnServer(config, "CodeQL CLI Server", ["execute", "cli-server"], [], this.logger, data => { })
}
private async runCodeQlCliInternal(command: string[], commandArgs: string[], description: string): Promise<string> {
let stderrBuffers: Buffer[] = [];
if (this.commandInProcess) {
throw new Error("runCodeQlCliInternal called while cli was running")
}
this.commandInProcess = true;
try {
//Launch the process if it doesn't exist
if (!this.process) {
this.process = await this.launchProcess()
}
// Grab the process so that typescript know that it is always defined.
const process = this.process;
// The array of fragments of stdout
let stdoutBuffers: Buffer[] = [];
// Compute the full args array
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
this.logger.log(`${description} using CodeQL CLI: ${argsString}...`);
try {
await new Promise((resolve, reject) => {
// Start listening to stdout
process.stdout.addListener('data', (newData: Buffer) => {
stdoutBuffers.push(newData);
// If the buffer ends in '0' then exit.
// We don't have to check the middle as no output will be written after the null until
// the next command starts
if (newData.length > 0 && newData.readUInt8(newData.length - 1) === 0) {
resolve();
}
});
// Listen to stderr
process.stderr.addListener('data', (newData: Buffer) => {
stderrBuffers.push(newData);
});
// Listen for process exit.
process.addListener("close", (code) => reject(code));
// Write the command followed by a null terminator.
process.stdin.write(JSON.stringify(args), "utf8")
process.stdin.write(this.nullBuffer)
});
// Join all the data together
let fullBuffer = Buffer.concat(stdoutBuffers);
// Make sure we remove the terminator;
let data = fullBuffer.toString("utf8", 0, fullBuffer.length - 1);
this.logger.log(`CLI command succeeded.`);
return data;
} catch (err) {
// Kill the process if it isn't already dead.
this.killProcessIfRunning();
// Report the error (if there is a stderr then use that otherwise just report the error cod or nodejs error)
if (stderrBuffers.length == 0) {
throw new Error(`${description} failed: ${err}`)
} else {
throw new Error(`${description} failed: ${Buffer.concat(stderrBuffers).toString("utf8")}`);
}
} finally {
this.logger.log(Buffer.concat(stderrBuffers).toString("utf8"));
// Remove the listeners we set up.
process.stdout.removeAllListeners('data')
process.stderr.removeAllListeners('data')
process.removeAllListeners("close");
}
} finally {
this.commandInProcess = false;
// start running the next command immediately
this.runNext();
}
}
/**
* Run the next command in the queue
*/
private runNext() {
const callback = this.commandQueue.shift();
if (callback) {
callback();
}
}
/**
* Runs a CodeQL CLI command on the server, returning the output as a string.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param description Description of the action being run, to be shown in log and error messages.
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @returns The contents of the command's stdout, if the command succeeded.
*/
runCodeQlCliCommand(command: string[], commandArgs: string[], description: string, progressReporter?: ProgressReporter): Promise<string> {
if (progressReporter) {
progressReporter.report({ message: description });
}
return new Promise((resolve, reject) => {
// Construct the command that actually does the work
const callback = () => {
try {
this.runCodeQlCliInternal(command, commandArgs, description).then(resolve, reject);
} catch (err) {
reject(err);
}
}
// If the server is not running a command, then run the given command immediately,
// otherwise add to the queue
if (this.commandInProcess) {
this.commandQueue.push(callback)
} else {
callback();
}
});
}
/**
* Runs a CodeQL CLI command, returning the output as JSON.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param description Description of the action being run, to be shown in log and error messages.
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @returns The contents of the command's stdout, if the command succeeded.
*/
async runJsonCodeQlCliCommand<OutputType>(command: string[], commandArgs: string[], description: string, progressReporter?: ProgressReporter): Promise<OutputType> {
// Add format argument first, in case commandArgs contains positional parameters.
const args = ['--format', 'json'].concat(commandArgs);
const result = await this.runCodeQlCliCommand(command, args, description, progressReporter);
try {
return JSON.parse(result) as OutputType;
} catch (err) {
throw new Error(`Parsing output of ${description} failed: ${err.stderr || err}`)
}
}
/**
* Resolve the library path and dbscheme for a query.
* @param workspaces The current open workspaces
* @param queryPath The path to the query
*/
async resolveLibraryPath(workspaces: string[], queryPath: string): Promise<QuerySetup> {
const subcommandArgs = [
'--query', queryPath,
"--additional-packs",
workspaces.join(path.delimiter)
];
return await this.runJsonCodeQlCliCommand<QuerySetup>(['resolve', 'library-path'], subcommandArgs, "Resolving library paths");
}
/**
* Gets the metadata for a query.
* @param queryPath The path to the query.
*/
async resolveMetadata(queryPath: string): Promise<QueryMetadata> {
return await this.runJsonCodeQlCliCommand<QueryMetadata>(['resolve', 'metadata'], [queryPath], "Resolving query metadata");
}
/**
* Gets the RAM setting for the query server.
* @param queryMemoryMb The maximum amount of RAM to use, in MB.
* Leave `undefined` for CodeQL to choose a limit based on the available system memory.
* @returns String arguments that can be passed to the CodeQL query server,
* indicating how to split the given RAM limit between heap and off-heap memory.
*/
async resolveRam(queryMemoryMb: number | undefined, progressReporter?: ProgressReporter): Promise<string[]> {
const args: string[] = [];
if (queryMemoryMb !== undefined) {
args.push('--ram', queryMemoryMb.toString());
}
return await this.runJsonCodeQlCliCommand<string[]>(['resolve', 'ram'], args, "Resolving RAM settings", progressReporter);
}
async interpretBqrs(metadata: { kind: string, id: string }, resultsPath: string, interpretedResultsPath: string, sourceInfo?: SourceInfo): Promise<sarif.Log> {
const args = [
`-t=kind=${metadata.kind}`,
`-t=id=${metadata.id}`,
"--output", interpretedResultsPath,
"--format", SARIF_FORMAT,
// TODO: This flag means that we don't group interpreted results
// by primary location. We may want to revisit whether we call
// interpretation with and without this flag, or do some
// grouping client-side.
"--no-group-results",
];
if (sourceInfo !== undefined) {
args.push(
"--source-archive", sourceInfo.sourceArchive,
"--source-location-prefix", sourceInfo.sourceLocationPrefix
);
}
args.push(resultsPath);
await this.runCodeQlCliCommand(['bqrs', 'interpret'], args, "Interpreting query results");
let output: string;
try {
output = await fs.readFile(interpretedResultsPath, 'utf8');
} catch (err) {
throw new Error(`Reading output of interpretation failed: ${err.stderr || err}`)
}
try {
return JSON.parse(output) as sarif.Log;
} catch (err) {
throw new Error(`Parsing output of interpretation failed: ${err.stderr || err}`)
}
}
async sortBqrs(resultsPath: string, sortedResultsPath: string, resultSet: string, sortKeys: number[], sortDirections: SortDirection[]): Promise<void> {
const sortDirectionStrings = sortDirections.map(direction => {
switch (direction) {
case SortDirection.asc:
return "asc";
case SortDirection.desc:
return "desc";
default:
return assertNever(direction);
}
});
await this.runCodeQlCliCommand(['bqrs', 'decode'],
[
"--format=bqrs",
`--result-set=${resultSet}`,
`--output=${sortedResultsPath}`,
`--sort-key=${sortKeys.join(",")}`,
`--sort-direction=${sortDirectionStrings.join(",")}`,
resultsPath
],
"Sorting query results");
}
/**
* Returns the `DbInfo` for a database.
* @param databasePath Path to the CodeQL database to obtain information from.
*/
resolveDatabase(databasePath: string): Promise<DbInfo> {
return this.runJsonCodeQlCliCommand(['resolve', 'database'], [databasePath],
"Resolving database");
}
/**
* Gets information necessary for upgrading a database.
* @param dbScheme the path to the dbscheme of the database to be upgraded.
* @param searchPath A list of directories to search for upgrade scripts.
* @returns A list of database upgrade script directories
*/
resolveUpgrades(dbScheme: string, searchPath: string[]): Promise<UpgradesInfo> {
const args = ['--additional-packs', searchPath.join(path.delimiter), '--dbscheme', dbScheme];
return this.runJsonCodeQlCliCommand<UpgradesInfo>(
['resolve', 'upgrades'],
args,
"Resolving database upgrade scripts",
);
}
}
/**
* Spawns a child server process using the CodeQL CLI
* and attaches listeners to it.
*
* @param config The configuration containing the path to the CLI.
* @param name Name of the server being started, to be shown in log and error messages.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param logger Logger to write startup messages.
* @param stderrListener Listener for log messages from the server's stderr stream.
* @param stdoutListener Optional listener for messages from the server's stdout stream.
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @returns The started child process.
*/
export function spawnServer(
codeqlPath: string,
name: string,
command: string[],
commandArgs: string[],
logger: Logger,
stderrListener: (data: any) => void,
stdoutListener?: (data: any) => void,
progressReporter?: ProgressReporter
): child_process.ChildProcessWithoutNullStreams {
// Enable verbose logging.
const args = command.concat(commandArgs).concat(LOGGING_FLAGS);
// Start the server process.
const base = codeqlPath;
const argsString = args.join(" ");
if (progressReporter !== undefined) {
progressReporter.report({ message: `Starting ${name}` });
}
logger.log(`Starting ${name} using CodeQL CLI: ${base} ${argsString}`);
const child = child_process.spawn(base, args);
if (!child || !child.pid) {
throw new Error(`Failed to start ${name} using command ${base} ${argsString}.`);
}
// Set up event listeners.
child.on('close', (code) => logger.log(`Child process exited with code ${code}`));
child.stderr!.on('data', stderrListener);
if (stdoutListener !== undefined) {
child.stdout!.on('data', stdoutListener);
}
if (progressReporter !== undefined) {
progressReporter.report({ message: `Started ${name}` });
}
logger.log(`${name} started on PID: ${child.pid}`);
return child;
}
/**
* Runs a CodeQL CLI command without invoking the CLI server, returning the output as a string.
* @param config The configuration containing the path to the CLI.
* @param command The `codeql` command to be run, provided as an array of command/subcommand names.
* @param commandArgs The arguments to pass to the `codeql` command.
* @param description Description of the action being run, to be shown in log and error messages.
* @param logger Logger to write command log messages, e.g. to an output channel.
* @param progressReporter Used to output progress messages, e.g. to the status bar.
* @returns The contents of the command's stdout, if the command succeeded.
*/
export async function runCodeQlCliCommand(codeQlPath: string, command: string[], commandArgs: string[], description: string, logger: Logger, progressReporter?: ProgressReporter): Promise<string> {
// Add logging arguments first, in case commandArgs contains positional parameters.
const args = command.concat(LOGGING_FLAGS).concat(commandArgs);
const argsString = args.join(" ");
try {
if (progressReporter !== undefined) {
progressReporter.report({ message: description });
}
logger.log(`${description} using CodeQL CLI: ${codeQlPath} ${argsString}...`);
const result = await util.promisify(child_process.execFile)(codeQlPath, args);
logger.log(result.stderr);
logger.log(`CLI command succeeded.`);
return result.stdout;
} catch (err) {
throw new Error(`${description} failed: ${err.stderr || err}`)
}
}

View File

@@ -0,0 +1,188 @@
import { DisposableObject } from 'semmle-vscode-utils';
import { workspace, Event, EventEmitter, ConfigurationChangeEvent } from 'vscode';
import { DistributionManager } from './distribution';
import { logger } from './logging';
/** Helper class to look up a labelled (and possibly nested) setting. */
class Setting {
name: string;
parent?: Setting;
constructor(name: string, parent?: Setting) {
this.name = name;
this.parent = parent;
}
get qualifiedName(): string {
if (this.parent === undefined) {
return this.name;
} else {
return `${this.parent.qualifiedName}.${this.name}`;
}
}
getValue<T>(): T {
if (this.parent === undefined) {
throw new Error('Cannot get the value of a root setting.');
}
return workspace.getConfiguration(this.parent.qualifiedName).get<T>(this.name)!;
}
}
const ROOT_SETTING = new Setting('codeQL');
// Distribution configuration
const DISTRIBUTION_SETTING = new Setting('cli', ROOT_SETTING);
const CUSTOM_CODEQL_PATH_SETTING = new Setting('executablePath', DISTRIBUTION_SETTING);
const INCLUDE_PRERELEASE_SETTING = new Setting('includePrerelease', DISTRIBUTION_SETTING);
const PERSONAL_ACCESS_TOKEN_SETTING = new Setting('personalAccessToken', DISTRIBUTION_SETTING);
const OWNER_NAME_SETTING = new Setting('owner', DISTRIBUTION_SETTING);
const REPOSITORY_NAME_SETTING = new Setting('repository', DISTRIBUTION_SETTING);
/** When these settings change, the distribution should be updated. */
const DISTRIBUTION_CHANGE_SETTINGS = [CUSTOM_CODEQL_PATH_SETTING, INCLUDE_PRERELEASE_SETTING, PERSONAL_ACCESS_TOKEN_SETTING, OWNER_NAME_SETTING, REPOSITORY_NAME_SETTING];
export interface DistributionConfig {
customCodeQlPath?: string;
includePrerelease: boolean;
personalAccessToken?: string;
ownerName: string;
repositoryName: string;
onDidChangeDistributionConfiguration?: Event<void>;
}
// Query server configuration
const RUNNING_QUERIES_SETTING = new Setting('runningQueries', ROOT_SETTING);
const NUMBER_OF_THREADS_SETTING = new Setting('numberOfThreads', RUNNING_QUERIES_SETTING);
const TIMEOUT_SETTING = new Setting('timeout', RUNNING_QUERIES_SETTING);
const MEMORY_SETTING = new Setting('memory', RUNNING_QUERIES_SETTING);
const DEBUG_SETTING = new Setting('debug', RUNNING_QUERIES_SETTING);
/** When these settings change, the running query server should be restarted. */
const QUERY_SERVER_RESTARTING_SETTINGS = [NUMBER_OF_THREADS_SETTING, MEMORY_SETTING, DEBUG_SETTING];
export interface QueryServerConfig {
codeQlPath: string,
debug: boolean,
numThreads: number,
queryMemoryMb?: number,
timeoutSecs: number,
onDidChangeQueryServerConfiguration?: Event<void>;
}
abstract class ConfigListener extends DisposableObject {
protected readonly _onDidChangeConfiguration = this.push(new EventEmitter<void>());
constructor() {
super();
this.updateConfiguration();
this.push(workspace.onDidChangeConfiguration(this.handleDidChangeConfiguration, this));
}
/**
* Calls `updateConfiguration` if any of the `relevantSettings` have changed.
*/
protected handleDidChangeConfigurationForRelevantSettings(relevantSettings: Setting[], e: ConfigurationChangeEvent): void {
// Check whether any options that affect query running were changed.
for (const option of relevantSettings) {
// TODO: compare old and new values, only update if there was actually a change?
if (e.affectsConfiguration(option.qualifiedName)) {
this.updateConfiguration();
break; // only need to do this once, if any of the settings have changed
}
}
}
protected abstract handleDidChangeConfiguration(e: ConfigurationChangeEvent): void;
private updateConfiguration(): void {
this._onDidChangeConfiguration.fire();
}
}
export class DistributionConfigListener extends ConfigListener implements DistributionConfig {
public get customCodeQlPath(): string | undefined {
return CUSTOM_CODEQL_PATH_SETTING.getValue() ? CUSTOM_CODEQL_PATH_SETTING.getValue() : undefined;
}
public get includePrerelease(): boolean {
return INCLUDE_PRERELEASE_SETTING.getValue();
}
public get personalAccessToken(): string | undefined {
return PERSONAL_ACCESS_TOKEN_SETTING.getValue() ? PERSONAL_ACCESS_TOKEN_SETTING.getValue() : undefined;
}
public get ownerName(): string {
return OWNER_NAME_SETTING.getValue();
}
public get repositoryName(): string {
return REPOSITORY_NAME_SETTING.getValue();
}
public get onDidChangeDistributionConfiguration(): Event<void> {
return this._onDidChangeConfiguration.event;
}
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(DISTRIBUTION_CHANGE_SETTINGS, e);
}
}
export class QueryServerConfigListener extends ConfigListener implements QueryServerConfig {
private constructor(private _codeQlPath: string) {
super();
}
public static async createQueryServerConfigListener(distributionManager: DistributionManager): Promise<QueryServerConfigListener> {
const codeQlPath = await distributionManager.getCodeQlPathWithoutVersionCheck();
const config = new QueryServerConfigListener(codeQlPath!);
if (distributionManager.onDidChangeDistribution) {
config.push(distributionManager.onDidChangeDistribution(async () => {
const codeQlPath = await distributionManager.getCodeQlPathWithoutVersionCheck();
config._codeQlPath = codeQlPath!;
config._onDidChangeConfiguration.fire();
}));
}
return config;
}
public get codeQlPath(): string {
return this._codeQlPath;
}
public get numThreads(): number {
return NUMBER_OF_THREADS_SETTING.getValue<number>();
}
/** Gets the configured query timeout, in seconds. This looks up the setting at the time of access. */
public get timeoutSecs(): number {
return TIMEOUT_SETTING.getValue<number | null>() || 0;
}
public get queryMemoryMb(): number | undefined {
const memory = MEMORY_SETTING.getValue<number | null>();
if (memory === null) {
return undefined;
}
if (memory == 0 || typeof (memory) !== 'number') {
logger.log(`Ignoring value '${memory}' for setting ${MEMORY_SETTING.qualifiedName}`);
return undefined;
}
return memory;
}
public get debug(): boolean {
return DEBUG_SETTING.getValue<boolean>();
}
public get onDidChangeQueryServerConfiguration(): Event<void> {
return this._onDidChangeConfiguration.event;
}
protected handleDidChangeConfiguration(e: ConfigurationChangeEvent): void {
this.handleDidChangeConfigurationForRelevantSettings(QUERY_SERVER_RESTARTING_SETTINGS, e);
}
}

View File

@@ -0,0 +1,263 @@
import * as path from 'path';
import { DisposableObject } from "semmle-vscode-utils";
import { commands, Event, EventEmitter, ExtensionContext, ProviderResult, TreeDataProvider, TreeItem, Uri, window } from "vscode";
import * as cli from './cli';
import { DatabaseItem, DatabaseManager } from "./databases";
import { logger } from "./logging";
import { clearCacheInDatabase, upgradeDatabase, UserCancellationException } from "./queries";
import * as qsClient from './queryserver-client';
import { getOnDiskWorkspaceFolders } from "./helpers";
type ThemableIconPath = { light: string, dark: string } | string;
/**
* Path to icons to display next to currently selected database.
*/
const SELECTED_DATABASE_ICON: ThemableIconPath = {
light: 'media/check-light-mode.svg',
dark: 'media/check-dark-mode.svg',
};
/**
* Path to icon to display next to an invalid database.
*/
const INVALID_DATABASE_ICON: ThemableIconPath = 'media/red-x.svg';
function joinThemableIconPath(base: string, iconPath: ThemableIconPath): ThemableIconPath {
if (typeof iconPath == 'object')
return {
light: path.join(base, iconPath.light),
dark: path.join(base, iconPath.dark)
};
else
return path.join(base, iconPath);
}
/**
* Tree data provider for the databases view.
*/
class DatabaseTreeDataProvider extends DisposableObject
implements TreeDataProvider<DatabaseItem> {
private readonly _onDidChangeTreeData = new EventEmitter<DatabaseItem | undefined>();
private currentDatabaseItem: DatabaseItem | undefined;
constructor(private ctx: ExtensionContext, private databaseManager: DatabaseManager) {
super();
this.currentDatabaseItem = databaseManager.currentDatabaseItem;
this.push(this.databaseManager.onDidChangeDatabaseItem(this.handleDidChangeDatabaseItem));
this.push(this.databaseManager.onDidChangeCurrentDatabaseItem(
this.handleDidChangeCurrentDatabaseItem));
}
public get onDidChangeTreeData(): Event<DatabaseItem | undefined> {
return this._onDidChangeTreeData.event;
}
private handleDidChangeDatabaseItem = (databaseItem: DatabaseItem | undefined): void => {
this._onDidChangeTreeData.fire(databaseItem);
}
private handleDidChangeCurrentDatabaseItem = (databaseItem: DatabaseItem | undefined): void => {
if (this.currentDatabaseItem) {
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
}
this.currentDatabaseItem = databaseItem;
if (this.currentDatabaseItem) {
this._onDidChangeTreeData.fire(this.currentDatabaseItem);
}
}
public getTreeItem(element: DatabaseItem): TreeItem {
const item = new TreeItem(element.name);
if (element === this.currentDatabaseItem) {
item.iconPath = joinThemableIconPath(this.ctx.extensionPath, SELECTED_DATABASE_ICON);
} else if (element.error !== undefined) {
item.iconPath = joinThemableIconPath(this.ctx.extensionPath, INVALID_DATABASE_ICON);
}
item.tooltip = element.databaseUri.fsPath;
return item;
}
public getChildren(element?: DatabaseItem): ProviderResult<DatabaseItem[]> {
if (element === undefined) {
return this.databaseManager.databaseItems.slice(0);
}
else {
return [];
}
}
public getParent(element: DatabaseItem): ProviderResult<DatabaseItem> {
return null;
}
public getCurrent(): DatabaseItem | undefined {
return this.currentDatabaseItem;
}
}
/** Gets the first element in the given list, if any, or undefined if the list is empty or undefined. */
function getFirst(list: Uri[] | undefined): Uri | undefined {
if (list === undefined || list.length === 0) {
return undefined;
}
else {
return list[0];
}
}
/**
* Displays file selection dialog. Expects the user to choose a
* database directory, which should be the parent directory of a
* directory of the form `db-[language]`, for example, `db-cpp`.
*
* XXX: no validation is done other than checking the directory name
* to make sure it really is a database directory.
*/
async function chooseDatabaseDir(): Promise<Uri | undefined> {
const chosen = await window.showOpenDialog({
openLabel: 'Choose Database',
canSelectFiles: true,
canSelectFolders: true,
canSelectMany: false
});
return getFirst(chosen);
}
export class DatabaseUI extends DisposableObject {
public constructor(private ctx: ExtensionContext, private cliserver: cli.CodeQLCliServer, private databaseManager: DatabaseManager,
private readonly queryServer: qsClient.QueryServerClient | undefined) {
super();
const treeDataProvider = this.push(new DatabaseTreeDataProvider(ctx, databaseManager));
this.push(window.createTreeView('codeQLDatabases', { treeDataProvider }));
ctx.subscriptions.push(commands.registerCommand('codeQL.chooseDatabase', this.handleChooseDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQL.setCurrentDatabase', this.handleSetCurrentDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQL.upgradeCurrentDatabase', this.handleUpgradeCurrentDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQL.clearCache', this.handleClearCache));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.setCurrentDatabase', this.handleMakeCurrentDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.removeDatabase', this.handleRemoveDatabase));
ctx.subscriptions.push(commands.registerCommand('codeQLDatabases.upgradeDatabase', this.handleUpgradeDatabase));
}
private handleMakeCurrentDatabase = async (databaseItem: DatabaseItem): Promise<void> => {
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
}
private handleChooseDatabase = async (): Promise<DatabaseItem | undefined> => {
return await this.chooseAndSetDatabase();
}
private handleUpgradeCurrentDatabase = async (): Promise<void> => {
await this.handleUpgradeDatabase(this.databaseManager.currentDatabaseItem);
}
private handleUpgradeDatabase = async (databaseItem: DatabaseItem | undefined): Promise<void> => {
if (this.queryServer === undefined) {
logger.log('Received request to upgrade database, but there is no running query server.');
return;
}
if (databaseItem === undefined) {
logger.log('Received request to upgrade database, but no database was provided.');
return;
}
if (databaseItem.contents === undefined) {
logger.log('Received request to upgrade database, but database contents could not be found.');
return;
}
if (databaseItem.contents.dbSchemeUri === undefined) {
logger.log('Received request to upgrade database, but database has no schema.');
return;
}
// Search for upgrade scripts in any workspace folders available
const searchPath: string[] = getOnDiskWorkspaceFolders();
const upgradeInfo = await this.cliserver.resolveUpgrades(
databaseItem.contents.dbSchemeUri.fsPath,
searchPath,
);
const { scripts, finalDbscheme } = upgradeInfo;
if (finalDbscheme === undefined) {
logger.log('Could not determine target dbscheme to upgrade to.');
return;
}
const parentDirs = scripts.map(dir => path.dirname(dir));
const uniqueParentDirs = new Set(parentDirs);
const targetDbSchemeUri = Uri.file(finalDbscheme);
const upgradesDirectories = Array.from(uniqueParentDirs).map(filePath => Uri.file(filePath));
try {
await upgradeDatabase(this.queryServer, databaseItem, targetDbSchemeUri, upgradesDirectories);
}
catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
}
else
throw e;
}
}
private handleClearCache = async (): Promise<void> => {
if ((this.queryServer !== undefined) &&
(this.databaseManager.currentDatabaseItem !== undefined)) {
await clearCacheInDatabase(this.queryServer, this.databaseManager.currentDatabaseItem);
}
}
private handleSetCurrentDatabase = async (uri: Uri): Promise<DatabaseItem | undefined> => {
return await this.setCurrentDatabase(uri);
}
private handleRemoveDatabase = (databaseItem: DatabaseItem): void => {
this.databaseManager.removeDatabaseItem(databaseItem);
}
/**
* Return the current database directory. If we don't already have a
* current database, ask the user for one, and return that, or
* undefined if they cancel.
*/
public async getDatabaseItem(): Promise<DatabaseItem | undefined> {
if (this.databaseManager.currentDatabaseItem === undefined) {
await this.chooseAndSetDatabase();
}
return this.databaseManager.currentDatabaseItem;
}
private async setCurrentDatabase(uri: Uri): Promise<DatabaseItem | undefined> {
let databaseItem = this.databaseManager.findDatabaseItem(uri);
if (databaseItem === undefined) {
databaseItem = await this.databaseManager.openDatabase(uri);
}
await this.databaseManager.setCurrentDatabaseItem(databaseItem);
return databaseItem;
}
/**
* Ask the user for a database directory. Returns the chosen database, or `undefined` if the
* operation was canceled.
*/
private async chooseAndSetDatabase(): Promise<DatabaseItem | undefined> {
const uri = await chooseDatabaseDir();
if (uri !== undefined) {
return await this.setCurrentDatabase(uri);
}
else {
return undefined;
}
}
}

View File

@@ -0,0 +1,631 @@
import * as fs from 'fs-extra';
import * as glob from 'glob-promise';
import * as path from 'path';
import * as vscode from 'vscode';
import * as cli from './cli';
import { ExtensionContext } from 'vscode';
import { showAndLogErrorMessage, showAndLogWarningMessage, showAndLogInformationMessage } from './helpers';
import { zipArchiveScheme, encodeSourceArchiveUri, decodeSourceArchiveUri } from './archive-filesystem-provider';
import { DisposableObject } from 'semmle-vscode-utils';
import { QueryServerConfig } from './config';
import { Logger, logger } from './logging';
/**
* databases.ts
* ------------
* Managing state of what the current database is, and what other
* databases have been recently selected.
*
* The source of truth of the current state resides inside the
* `DatabaseManager` class below.
*/
/**
* The name of the key in the workspaceState dictionary in which we
* persist the current database across sessions.
*/
const CURRENT_DB: string = 'currentDatabase';
/**
* The name of the key in the workspaceState dictionary in which we
* persist the list of databases across sessions.
*/
const DB_LIST: string = 'databaseList';
export interface DatabaseOptions {
displayName?: string;
ignoreSourceArchive?: boolean;
}
interface FullDatabaseOptions extends DatabaseOptions {
ignoreSourceArchive: boolean;
}
interface PersistedDatabaseItem {
uri: string;
options?: DatabaseOptions;
}
/**
* The layout of the database.
*/
export enum DatabaseKind {
/** A CodeQL database */
Database,
/** A raw QL dataset */
RawDataset
}
export interface DatabaseContents {
/** The layout of the database */
kind: DatabaseKind;
/**
* The name of the database.
*/
name: string;
/** The URI of the QL dataset within the database. */
datasetUri: vscode.Uri;
/** The URI of the source archive within the database, if one exists. */
sourceArchiveUri?: vscode.Uri;
/** The URI of the CodeQL database scheme within the database, if exactly one exists. */
dbSchemeUri?: vscode.Uri;
}
/**
* An error thrown when we cannot find a valid database in a putative
* database directory.
*/
class InvalidDatabaseError extends Error {
}
async function findDataset(parentDirectory: string): Promise<vscode.Uri> {
/*
* Look directly in the root
*/
let dbRelativePaths = await glob('db-*/', {
cwd: parentDirectory
});
if (dbRelativePaths.length === 0) {
/*
* Check If they are in the old location
*/
dbRelativePaths = await glob('working/db-*/', {
cwd: parentDirectory
});
}
if (dbRelativePaths.length === 0) {
throw new InvalidDatabaseError(`'${parentDirectory}' does not contain a dataset directory.`);
}
const dbAbsolutePath = path.join(parentDirectory, dbRelativePaths[0]);
if (dbRelativePaths.length > 1) {
showAndLogWarningMessage(`Found multiple dataset directories in database, using '${dbAbsolutePath}'.`);
}
return vscode.Uri.file(dbAbsolutePath);
}
async function findSourceArchive(databasePath: string, silent: boolean = false):
Promise<vscode.Uri | undefined> {
const relativePaths = ['src', 'output/src_archive']
for (const relativePath of relativePaths) {
const basePath = path.join(databasePath, relativePath);
const zipPath = basePath + '.zip';
if (await fs.pathExists(basePath)) {
return vscode.Uri.file(basePath);
}
else if (await fs.pathExists(zipPath)) {
return vscode.Uri.file(zipPath).with({ scheme: zipArchiveScheme });
}
}
if (!silent)
showAndLogInformationMessage(`Could not find source archive for database '${databasePath}'. Assuming paths are absolute.`);
return undefined;
}
async function resolveDatabase(databasePath: string):
Promise<DatabaseContents | undefined> {
const name = path.basename(databasePath);
// Look for dataset and source archive.
const datasetUri = await findDataset(databasePath);
const sourceArchiveUri = await findSourceArchive(databasePath);
return {
kind: DatabaseKind.Database,
name,
datasetUri,
sourceArchiveUri
};
}
/** Gets the relative paths of all `.dbscheme` files in the given directory. */
async function getDbSchemeFiles(dbDirectory: string): Promise<string[]> {
return await glob('*.dbscheme', { cwd: dbDirectory });
}
async function resolveRawDataset(datasetPath: string): Promise<DatabaseContents | undefined> {
if ((await getDbSchemeFiles(datasetPath)).length > 0) {
return {
kind: DatabaseKind.RawDataset,
name: path.basename(datasetPath),
datasetUri: vscode.Uri.file(datasetPath),
sourceArchiveUri: undefined
};
}
else {
return undefined;
}
}
async function resolveDatabaseContents(uri: vscode.Uri): Promise<DatabaseContents> {
if (uri.scheme !== 'file') {
throw new Error(`Database URI scheme '${uri.scheme}' not supported; only 'file' URIs are supported.`);
}
const databasePath = uri.fsPath;
if (!await fs.pathExists(databasePath)) {
throw new InvalidDatabaseError(`Database '${databasePath}' does not exist.`);
}
const contents = await resolveDatabase(databasePath) || await resolveRawDataset(databasePath);
if (contents === undefined) {
throw new InvalidDatabaseError(`'${databasePath}' is not a valid database.`);
}
// Look for a single dbscheme file within the database.
// This should be found in the dataset directory, regardless of the form of database.
const dbPath = contents.datasetUri.fsPath;
const dbSchemeFiles = await getDbSchemeFiles(dbPath);
if (dbSchemeFiles.length === 0) {
throw new InvalidDatabaseError(`Database '${databasePath}' does not contain a CodeQL dbscheme under '${dbPath}'.`);
}
else if (dbSchemeFiles.length > 1) {
throw new InvalidDatabaseError(`Database '${databasePath}' contains multiple CodeQL dbschemes under '${dbPath}'.`);
} else {
contents.dbSchemeUri = vscode.Uri.file(path.resolve(dbPath, dbSchemeFiles[0]));
}
return contents;
}
/** An item in the list of available databases */
export interface DatabaseItem {
/** The URI of the database */
readonly databaseUri: vscode.Uri;
/** The name of the database to be displayed in the UI */
readonly name: string;
/** The URI of the database's source archive, or `undefined` if no source archive is to be used. */
readonly sourceArchive: vscode.Uri | undefined;
/**
* The contents of the database.
* Will be `undefined` if the database is invalid. Can be updated by calling `refresh()`.
*/
readonly contents: DatabaseContents | undefined;
/** If the database is invalid, describes why. */
readonly error: Error | undefined;
/**
* Resolves the contents of the database.
*
* @remarks
* The contents include the database directory, source archive, and metadata about the database.
* If the database is invalid, `this.error` is updated with the error object that describes why
* the database is invalid. This error is also thrown.
*/
refresh(): Promise<void>;
/**
* Resolves a filename to its URI in the source archive.
*
* @param file Filename within the source archive. May be `undefined` to return a dummy file path.
*/
resolveSourceFile(file: string | undefined): vscode.Uri;
/**
* Holds if the database item has a `.dbinfo` file.
*/
hasDbInfo(): boolean;
/**
* Returns `sourceLocationPrefix` of exported database.
*/
getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string>;
/**
* Returns the root uri of the virtual filesystem for this database's source archive,
* as displayed in the filesystem explorer.
*/
getSourceArchiveExplorerUri(): vscode.Uri | undefined;
/**
* Holds if `uri` belongs to this database's source archive.
*/
belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean;
}
class DatabaseItemImpl implements DatabaseItem {
private _error: Error | undefined = undefined;
private _contents: DatabaseContents | undefined;
/** A cache of database info */
private _dbinfo: cli.DbInfo | undefined;
public constructor(public readonly databaseUri: vscode.Uri,
contents: DatabaseContents | undefined, private options: FullDatabaseOptions,
private readonly onChanged: (item: DatabaseItemImpl) => void) {
this._contents = contents;
}
public get name(): string {
if (this.options.displayName) {
return this.options.displayName;
}
else if (this._contents) {
return this._contents.name;
}
else {
return path.basename(this.databaseUri.fsPath);
}
}
public get sourceArchive(): vscode.Uri | undefined {
if (this.options.ignoreSourceArchive || (this._contents === undefined)) {
return undefined;
}
else {
return this._contents.sourceArchiveUri;
}
}
public get contents(): DatabaseContents | undefined {
return this._contents;
}
public get error(): Error | undefined {
return this._error;
}
public async refresh(): Promise<void> {
try {
try {
this._contents = await resolveDatabaseContents(this.databaseUri);
this._error = undefined;
}
catch (e) {
this._contents = undefined;
this._error = e;
throw e;
}
}
finally {
this.onChanged(this);
}
}
public resolveSourceFile(file: string | undefined): vscode.Uri {
const sourceArchive = this.sourceArchive;
if (sourceArchive === undefined) {
if (file !== undefined) {
// Treat it as an absolute path.
return vscode.Uri.file(file);
}
else {
return this.databaseUri;
}
}
else {
if (file !== undefined) {
const absoluteFilePath = file.replace(':', '_');
// Strip any leading slashes from the file path, and replace `:` with `_`.
const relativeFilePath = absoluteFilePath.replace(/^\/*/, '').replace(':', '_');
if (sourceArchive.scheme == zipArchiveScheme) {
return encodeSourceArchiveUri({
pathWithinSourceArchive: absoluteFilePath,
sourceArchiveZipPath: sourceArchive.fsPath,
});
}
else {
let newPath = sourceArchive.path;
if (!newPath.endsWith('/')) {
// Ensure a trailing slash.
newPath += '/';
}
newPath += relativeFilePath;
return sourceArchive.with({ path: newPath });
}
}
else {
return sourceArchive;
}
}
}
/**
* Gets the state of this database, to be persisted in the workspace state.
*/
public getPersistedState(): PersistedDatabaseItem {
return {
uri: this.databaseUri.toString(true),
options: this.options
};
}
/**
* Holds if the database item refers to an exported snapshot
*/
public hasDbInfo(): boolean {
return fs.existsSync(path.join(this.databaseUri.fsPath, '.dbinfo'))
|| fs.existsSync(path.join(this.databaseUri.fsPath, 'codeql-database.yml'));;
}
/**
* Returns information about a database.
*/
private async getDbInfo(server: cli.CodeQLCliServer): Promise<cli.DbInfo> {
if (this._dbinfo === undefined) {
this._dbinfo = await server.resolveDatabase(this.databaseUri.fsPath);
}
return this._dbinfo;
}
/**
* Returns `sourceLocationPrefix` of database. Requires that the database
* has a `.dbinfo` file, which is the source of the prefix.
*/
public async getSourceLocationPrefix(server: cli.CodeQLCliServer): Promise<string> {
const dbInfo = await this.getDbInfo(server);
return dbInfo.sourceLocationPrefix;
}
/**
* Returns the root uri of the virtual filesystem for this database's source archive.
*/
public getSourceArchiveExplorerUri(): vscode.Uri | undefined {
const sourceArchive = this.sourceArchive;
if (sourceArchive === undefined || !sourceArchive.fsPath.endsWith('.zip'))
return undefined;
return encodeSourceArchiveUri({
pathWithinSourceArchive: '/',
sourceArchiveZipPath: sourceArchive.fsPath,
});
}
/**
* Holds if `uri` belongs to this database's source archive.
*/
public belongsToSourceArchiveExplorerUri(uri: vscode.Uri): boolean {
if (this.sourceArchive === undefined)
return false;
return uri.scheme === zipArchiveScheme &&
decodeSourceArchiveUri(uri).sourceArchiveZipPath === this.sourceArchive.fsPath;
}
}
/**
* A promise that resolves to an event's result value when the event
* `event` fires. If waiting for the event takes too long (by default
* >1000ms) log a warning, and resolve to undefined.
*/
function eventFired<T>(event: vscode.Event<T>, timeoutMs: number = 1000): Promise<T | undefined> {
return new Promise((res, rej) => {
let timeout: NodeJS.Timeout | undefined;
let disposable: vscode.Disposable | undefined;
function dispose() {
if (timeout !== undefined) clearTimeout(timeout);
if (disposable !== undefined) disposable.dispose();
}
disposable = event(e => {
res(e); dispose();
});
timeout = setTimeout(() => {
logger.log(`Waiting for event ${event} timed out after ${timeoutMs}ms`);
res(undefined); dispose();
}, timeoutMs);
});
}
export class DatabaseManager extends DisposableObject {
private readonly _onDidChangeDatabaseItem =
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
readonly onDidChangeDatabaseItem = this._onDidChangeDatabaseItem.event;
private readonly _onDidChangeCurrentDatabaseItem =
this.push(new vscode.EventEmitter<DatabaseItem | undefined>());
readonly onDidChangeCurrentDatabaseItem = this._onDidChangeCurrentDatabaseItem.event;
private readonly _databaseItems: DatabaseItemImpl[] = [];
private _currentDatabaseItem: DatabaseItem | undefined = undefined;
constructor(private ctx: ExtensionContext,
public config: QueryServerConfig,
public logger: Logger) {
super();
this.loadPersistedState(); // Let this run async.
}
public async openDatabase(uri: vscode.Uri, options?: DatabaseOptions):
Promise<DatabaseItem> {
const contents = await resolveDatabaseContents(uri);
const realOptions = options || {};
// Ignore the source archive for QLTest databases by default.
const isQLTestDatabase = path.extname(uri.fsPath) === '.testproj';
const fullOptions: FullDatabaseOptions = {
ignoreSourceArchive: (realOptions.ignoreSourceArchive !== undefined) ?
realOptions.ignoreSourceArchive : isQLTestDatabase,
displayName: realOptions.displayName
};
const databaseItem = new DatabaseItemImpl(uri, contents, fullOptions, (item) => {
this._onDidChangeDatabaseItem.fire(item);
});
await this.addDatabaseItem(databaseItem);
await this.addDatabaseSourceArchiveFolder(databaseItem);
return databaseItem;
}
private async addDatabaseSourceArchiveFolder(item: DatabaseItem) {
// The folder may already be in workspace state from a previous
// session. If not, add it.
const index = this.getDatabaseWorkspaceFolderIndex(item);
if (index === -1) {
// Add that filesystem as a folder to the current workspace.
//
// It's important that we add workspace folders to the end,
// rather than beginning of the list, because the first
// workspace folder is special; if it gets updated, the entire
// extension host is restarted. (cf.
// https://github.com/microsoft/vscode/blob/e0d2ed907d1b22808c56127678fb436d604586a7/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts#L209-L214)
//
// This is undesirable, as we might be adding and removing many
// workspace folders as the user adds and removes databases.
const end = (vscode.workspace.workspaceFolders || []).length;
const uri = item.getSourceArchiveExplorerUri();
if (uri === undefined) {
logger.log(`Couldn't obtain file explorer uri for ${item.name}`);
}
else {
logger.log(`Adding workspace folder for ${item.name} source archive at index ${end}`);
if ((vscode.workspace.workspaceFolders || []).length < 2) {
// Adding this workspace folder makes the workspace
// multi-root, which may surprise the user. Let them know
// we're doing this.
vscode.window.showInformationMessage(`Adding workspace folder for source archive of database ${item.name}.`);
}
vscode.workspace.updateWorkspaceFolders(end, 0, {
name: `[${item.name} source archive]`,
uri,
});
// vscode api documentation says we must to wait for this event
// between multiple `updateWorkspaceFolders` calls.
await eventFired(vscode.workspace.onDidChangeWorkspaceFolders);
}
}
}
private async createDatabaseItemFromPersistedState(state: PersistedDatabaseItem):
Promise<DatabaseItem> {
let displayName: string | undefined = undefined;
let ignoreSourceArchive = false;
if (state.options) {
if (typeof state.options.displayName === 'string') {
displayName = state.options.displayName;
}
if (typeof state.options.ignoreSourceArchive === 'boolean') {
ignoreSourceArchive = state.options.ignoreSourceArchive;
}
}
const fullOptions: FullDatabaseOptions = {
ignoreSourceArchive: ignoreSourceArchive,
displayName: displayName
};
const item = new DatabaseItemImpl(vscode.Uri.parse(state.uri), undefined, fullOptions,
(item) => {
this._onDidChangeDatabaseItem.fire(item)
});
await this.addDatabaseItem(item);
return item;
}
private async loadPersistedState(): Promise<void> {
const currentDatabaseUri = this.ctx.workspaceState.get<string>(CURRENT_DB);
const databases = this.ctx.workspaceState.get<PersistedDatabaseItem[]>(DB_LIST, []);
try {
for (const database of databases) {
const databaseItem = await this.createDatabaseItemFromPersistedState(database);
try {
await databaseItem.refresh();
if (currentDatabaseUri === database.uri) {
this.setCurrentDatabaseItem(databaseItem, true);
}
}
catch (e) {
// When loading from persisted state, leave invalid databases in the list. They will be
// marked as invalid, and cannot be set as the current database.
}
}
} catch (e) {
// database list had an unexpected type - nothing to be done?
showAndLogErrorMessage('Database list loading failed: ${}', e.message);
}
}
public get databaseItems(): readonly DatabaseItem[] {
return this._databaseItems;
}
public get currentDatabaseItem(): DatabaseItem | undefined {
return this._currentDatabaseItem;
}
public async setCurrentDatabaseItem(item: DatabaseItem | undefined,
skipRefresh: boolean = false): Promise<void> {
if (!skipRefresh && (item !== undefined)) {
await item.refresh(); // Will throw on invalid database.
}
if (this._currentDatabaseItem !== item) {
this._currentDatabaseItem = item;
this.updatePersistedCurrentDatabaseItem();
this._onDidChangeCurrentDatabaseItem.fire(item);
}
}
/**
* Returns the index of the workspace folder that corresponds to the source archive of `item`
* if there is one, and -1 otherwise.
*/
private getDatabaseWorkspaceFolderIndex(item: DatabaseItem): number {
return (vscode.workspace.workspaceFolders || [])
.findIndex(folder => item.belongsToSourceArchiveExplorerUri(folder.uri));
}
public findDatabaseItem(uri: vscode.Uri): DatabaseItem | undefined {
const uriString = uri.toString(true);
return this._databaseItems.find(item => item.databaseUri.toString(true) === uriString);
}
private async addDatabaseItem(item: DatabaseItemImpl) {
this._databaseItems.push(item);
this.updatePersistedDatabaseList();
this._onDidChangeDatabaseItem.fire(undefined);
}
public removeDatabaseItem(item: DatabaseItem) {
if (this._currentDatabaseItem == item)
this._currentDatabaseItem = undefined;
const index = this.databaseItems.findIndex(searchItem => searchItem === item);
if (index >= 0) {
this._databaseItems.splice(index, 1);
}
this.updatePersistedDatabaseList();
// Delete folder from workspace, if it is still there
const folderIndex = (vscode.workspace.workspaceFolders || []).findIndex(folder => item.belongsToSourceArchiveExplorerUri(folder.uri));
if (index >= 0) {
logger.log(`Removing workspace folder at index ${folderIndex}`);
vscode.workspace.updateWorkspaceFolders(folderIndex, 1);
}
this._onDidChangeDatabaseItem.fire(undefined);
}
private updatePersistedCurrentDatabaseItem(): void {
this.ctx.workspaceState.update(CURRENT_DB, this._currentDatabaseItem ?
this._currentDatabaseItem.databaseUri.toString(true) : undefined);
}
private updatePersistedDatabaseList(): void {
this.ctx.workspaceState.update(DB_LIST, this._databaseItems.map(item => item.getPersistedState()));
}
}

View File

@@ -0,0 +1,677 @@
import * as fetch from "node-fetch";
import * as fs from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as unzipper from "unzipper";
import * as url from "url";
import { ExtensionContext, Event } from "vscode";
import { DistributionConfig } from "./config";
import { ProgressUpdate, showAndLogErrorMessage } from "./helpers";
import { logger } from "./logging";
import { getCodeQlCliVersion, tryParseVersionString, Version } from "./cli-version";
/**
* distribution.ts
* ------------
*
* Management of CodeQL CLI binaries.
*/
/**
* Default value for the owner name of the extension-managed distribution on GitHub.
*
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_OWNER_NAME = "github";
/**
* Default value for the repository name of the extension-managed distribution on GitHub.
*
* We set the default here rather than as a default config value so that this default is invoked
* upon blanking the setting.
*/
const DEFAULT_DISTRIBUTION_REPOSITORY_NAME = "codeql-cli-binaries";
/**
* Version constraint for the CLI.
*
* This applies to both extension-managed and CLI distributions.
*/
export const DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT: VersionConstraint = {
description: "2.0.*",
isVersionCompatible: (v: Version) => {
return v.majorVersion === 2 && v.minorVersion === 0
}
}
export interface DistributionProvider {
getCodeQlPathWithoutVersionCheck(): Promise<string | undefined>,
onDidChangeDistribution?: Event<void>
}
export class DistributionManager implements DistributionProvider {
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
this._config = config;
this._extensionSpecificDistributionManager = new ExtensionSpecificDistributionManager(extensionContext, config, versionConstraint);
this._onDidChangeDistribution = config.onDidChangeDistributionConfiguration;
this._versionConstraint = versionConstraint;
}
/**
* Look up a CodeQL launcher binary.
*/
public async getDistribution(): Promise<FindDistributionResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
if (codeQlPath === undefined) {
return {
kind: FindDistributionResultKind.NoDistribution,
};
}
const version = await getCodeQlCliVersion(codeQlPath, logger);
if (version !== undefined && !this._versionConstraint.isVersionCompatible(version)) {
return {
codeQlPath,
kind: FindDistributionResultKind.IncompatibleDistribution,
version,
};
}
if (version === undefined) {
return {
codeQlPath,
kind: FindDistributionResultKind.UnknownCompatibilityDistribution,
}
}
return {
codeQlPath,
kind: FindDistributionResultKind.CompatibleDistribution,
version
};
}
/**
* Returns the path to a possibly-compatible CodeQL launcher binary, or undefined if a binary not be found.
*/
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
// Check config setting, then extension specific distribution, then PATH.
if (this._config.customCodeQlPath !== undefined) {
if (!await fs.pathExists(this._config.customCodeQlPath)) {
showAndLogErrorMessage(`The CodeQL executable path is specified as "${this._config.customCodeQlPath}" ` +
"by a configuration setting, but a CodeQL executable could not be found at that path. Please check " +
"that a CodeQL executable exists at the specified path or remove the setting.");
return undefined;
}
return this._config.customCodeQlPath;
}
const extensionSpecificCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (extensionSpecificCodeQlPath !== undefined) {
return extensionSpecificCodeQlPath;
}
if (process.env.PATH) {
for (const searchDirectory of process.env.PATH.split(path.delimiter)) {
const expectedLauncherPath = path.join(searchDirectory, codeQlLauncherName());
if (await fs.pathExists(expectedLauncherPath)) {
return expectedLauncherPath;
}
}
logger.log("INFO: Could not find CodeQL on path.");
}
return undefined;
}
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToExtensionManagedDistribution(): Promise<DistributionUpdateCheckResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
const extensionManagedCodeQlPath = await this._extensionSpecificDistributionManager.getCodeQlPathWithoutVersionCheck();
if (codeQlPath !== undefined && codeQlPath !== extensionManagedCodeQlPath) {
// A distribution is present but it isn't managed by the extension.
return createInvalidDistributionLocationResult();
}
return this._extensionSpecificDistributionManager.checkForUpdatesToDistribution();
}
/**
* Installs a release of the extension-managed distribution.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public installExtensionManagedDistributionRelease(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
return this._extensionSpecificDistributionManager.installDistributionRelease(release, progressCallback);
}
public get onDidChangeDistribution(): Event<void> | undefined {
return this._onDidChangeDistribution;
}
private readonly _config: DistributionConfig;
private readonly _extensionSpecificDistributionManager: ExtensionSpecificDistributionManager;
private readonly _onDidChangeDistribution: Event<void> | undefined;
private readonly _versionConstraint: VersionConstraint;
}
class ExtensionSpecificDistributionManager {
constructor(extensionContext: ExtensionContext, config: DistributionConfig, versionConstraint: VersionConstraint) {
this._extensionContext = extensionContext;
this._config = config;
this._versionConstraint = versionConstraint;
}
public async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
if (this.getInstalledRelease() !== undefined) {
// An extension specific distribution has been installed.
const expectedLauncherPath = path.join(this.getDistributionRootPath(), codeQlLauncherName());
if (await fs.pathExists(expectedLauncherPath)) {
return expectedLauncherPath;
}
logger.log(`WARNING: Expected to find a CodeQL CLI executable at ${expectedLauncherPath} but one was not found. ` +
"Will try PATH.");
try {
await this.removeDistribution();
} catch (e) {
logger.log("WARNING: Tried to remove corrupted CodeQL CLI at " +
`${this.getDistributionStoragePath()} but encountered an error: ${e}.`);
}
}
return undefined;
}
/**
* Check for updates to the extension-managed distribution. If one has not already been installed,
* this will return an update available result with the latest available release.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async checkForUpdatesToDistribution(): Promise<DistributionUpdateCheckResult> {
const codeQlPath = await this.getCodeQlPathWithoutVersionCheck();
const extensionSpecificRelease = this.getInstalledRelease();
const latestRelease = await this.getLatestRelease();
if (extensionSpecificRelease !== undefined && codeQlPath !== undefined && latestRelease.id === extensionSpecificRelease.id) {
return createDistributionAlreadyUpToDateResult();
}
return createUpdateAvailableResult(latestRelease);
}
/**
* Installs a release of the extension-managed distribution.
*
* Returns a failed promise if an unexpected error occurs during installation.
*/
public async installDistributionRelease(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
await this.downloadDistribution(release, progressCallback);
// Store the installed release within the global extension state.
this.storeInstalledRelease(release);
}
private async downloadDistribution(release: Release,
progressCallback?: (p: ProgressUpdate) => void): Promise<void> {
try {
await this.removeDistribution();
} catch (e) {
logger.log(`Tried to clean up old version of CLI at ${this.getDistributionStoragePath()} ` +
`but encountered an error: ${e}.`);
}
const assetStream = await this.createReleasesApiConsumer().streamBinaryContentOfAsset(release.assets[0]);
const tmpDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "vscode-codeql"));
try {
const archivePath = path.join(tmpDirectory, "distributionDownload.zip");
const archiveFile = fs.createWriteStream(archivePath);
const contentLength = assetStream.headers.get("content-length");
let numBytesDownloaded = 0;
if (progressCallback && contentLength !== null) {
const totalNumBytes = parseInt(contentLength, 10);
const bytesToDisplayMB = (numBytes: number) => `${(numBytes/(1024*1024)).toFixed(1)} MB`;
const updateProgress = () => {
progressCallback({
step: numBytesDownloaded,
maxStep: totalNumBytes,
message: `Downloading CodeQL CLI… [${bytesToDisplayMB(numBytesDownloaded)} of ${bytesToDisplayMB(totalNumBytes)}]`,
});
};
// Display the progress straight away rather than waiting for the first chunk.
updateProgress();
assetStream.body.on("data", data => {
numBytesDownloaded += data.length;
updateProgress();
});
}
await new Promise((resolve, reject) =>
assetStream.body.pipe(archiveFile)
.on("finish", resolve)
.on("error", reject)
);
this.bumpDistributionFolderIndex();
logger.log(`Extracting CodeQL CLI to ${this.getDistributionStoragePath()}`);
await extractZipArchive(archivePath, this.getDistributionStoragePath());
} finally {
await fs.remove(tmpDirectory);
}
}
/**
* Remove the extension-managed distribution.
*
* This should not be called for a distribution that is currently in use, as remove may fail.
*/
private async removeDistribution(): Promise<void> {
this.storeInstalledRelease(undefined);
if (await fs.pathExists(this.getDistributionStoragePath())) {
await fs.remove(this.getDistributionStoragePath());
}
}
private async getLatestRelease(): Promise<Release> {
const release = await this.createReleasesApiConsumer().getLatestRelease(this._versionConstraint, this._config.includePrerelease);
if (release.assets.length !== 1) {
throw new Error("Release had an unexpected number of assets");
}
return release;
}
private createReleasesApiConsumer(): ReleasesApiConsumer {
const ownerName = this._config.ownerName ? this._config.ownerName : DEFAULT_DISTRIBUTION_OWNER_NAME;
const repositoryName = this._config.repositoryName ? this._config.repositoryName : DEFAULT_DISTRIBUTION_REPOSITORY_NAME;
return new ReleasesApiConsumer(ownerName, repositoryName, this._config.personalAccessToken);
}
private bumpDistributionFolderIndex(): void {
const index = this._extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0);
this._extensionContext.globalState.update(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, index + 1);
}
private getDistributionStoragePath(): string {
// Use an empty string for the initial distribution for backwards compatibility.
const distributionFolderIndex = this._extensionContext.globalState.get(
ExtensionSpecificDistributionManager._currentDistributionFolderIndexStateKey, 0) || "";
return path.join(this._extensionContext.globalStoragePath,
ExtensionSpecificDistributionManager._currentDistributionFolderBaseName + distributionFolderIndex);
}
private getDistributionRootPath(): string {
return path.join(this.getDistributionStoragePath(),
ExtensionSpecificDistributionManager._codeQlExtractedFolderName);
}
private getInstalledRelease(): Release | undefined {
return this._extensionContext.globalState.get(ExtensionSpecificDistributionManager._installedReleaseStateKey);
}
private storeInstalledRelease(release: Release | undefined): void {
this._extensionContext.globalState.update(ExtensionSpecificDistributionManager._installedReleaseStateKey, release);
}
private readonly _config: DistributionConfig;
private readonly _extensionContext: ExtensionContext;
private readonly _versionConstraint: VersionConstraint;
private static readonly _currentDistributionFolderBaseName = "distribution";
private static readonly _currentDistributionFolderIndexStateKey = "distributionFolderIndex";
private static readonly _installedReleaseStateKey = "distributionRelease";
private static readonly _codeQlExtractedFolderName = "codeql";
}
export class ReleasesApiConsumer {
constructor(ownerName: string, repoName: string, personalAccessToken?: string) {
// Specify version of the GitHub API
this._defaultHeaders["accept"] = "application/vnd.github.v3+json";
if (personalAccessToken) {
this._defaultHeaders["authorization"] = `token ${personalAccessToken}`;
}
this._ownerName = ownerName;
this._repoName = repoName;
}
public async getLatestRelease(versionConstraint: VersionConstraint, includePrerelease: boolean = false): Promise<Release> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases`;
const allReleases: GithubRelease[] = await (await this.makeApiCall(apiPath)).json();
const compatibleReleases = allReleases.filter(release => {
if (release.prerelease && !includePrerelease) {
return false;
}
const version = tryParseVersionString(release.tag_name);
if (version === undefined || !versionConstraint.isVersionCompatible(version)) {
return false;
}
return true;
});
// tryParseVersionString must succeed due to the previous filtering step
const latestRelease = compatibleReleases.sort((a, b) => {
const versionComparison = versionCompare(tryParseVersionString(b.tag_name)!, tryParseVersionString(a.tag_name)!);
if (versionComparison === 0) {
return b.created_at.localeCompare(a.created_at);
}
return versionComparison;
})[0];
if (latestRelease === undefined) {
throw new Error("No compatible CodeQL CLI releases were found. " +
"Please check that the CodeQL extension is up to date.");
}
const assets: ReleaseAsset[] = latestRelease.assets.map(asset => {
return {
id: asset.id,
name: asset.name,
size: asset.size
};
});
return {
assets,
createdAt: latestRelease.created_at,
id: latestRelease.id,
name: latestRelease.name
};
}
public async streamBinaryContentOfAsset(asset: ReleaseAsset): Promise<fetch.Response> {
const apiPath = `/repos/${this._ownerName}/${this._repoName}/releases/assets/${asset.id}`;
return await this.makeApiCall(apiPath, {
"accept": "application/octet-stream"
});
}
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
const response = await this.makeRawRequest(ReleasesApiConsumer._apiBase + apiPath,
Object.assign({}, this._defaultHeaders, additionalHeaders));
if (!response.ok) {
throw new GithubApiError(response.status, await response.text());
}
return response;
}
private async makeRawRequest(
requestUrl: string,
headers: { [key: string]: string },
redirectCount: number = 0): Promise<fetch.Response> {
const response = await fetch.default(requestUrl, {
headers,
redirect: "manual"
});
const redirectUrl = response.headers.get("location");
if (isRedirectStatusCode(response.status) && redirectUrl && redirectCount < ReleasesApiConsumer._maxRedirects) {
const parsedRedirectUrl = url.parse(redirectUrl);
if (parsedRedirectUrl.protocol != "https:") {
throw new Error("Encountered a non-https redirect, rejecting");
}
if (parsedRedirectUrl.host != "api.github.com") {
// Remove authorization header if we are redirected outside of the GitHub API.
//
// This is necessary to stream release assets since AWS fails if more than one auth
// mechanism is provided.
delete headers["authorization"];
}
return await this.makeRawRequest(redirectUrl, headers, redirectCount + 1)
}
return response;
}
private readonly _defaultHeaders: { [key: string]: string } = {};
private readonly _ownerName: string;
private readonly _repoName: string;
private static readonly _apiBase = "https://api.github.com";
private static readonly _maxRedirects = 20;
}
export async function extractZipArchive(archivePath: string, outPath: string): Promise<void> {
const archive = await unzipper.Open.file(archivePath);
// This cast is necessary as the type definition for unzipper.Open.file(...).extract() is incorrect.
// It can be removed when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/40240 is merged.
await (archive.extract({
concurrency: 4,
path: outPath
}) as unknown as Promise<void>);
// Set file permissions for extracted files
await Promise.all(archive.files.map(async file => {
// Only change file permissions if within outPath (path.join normalises the path)
const extractedPath = path.join(outPath, file.path);
if (extractedPath.indexOf(outPath) !== 0 || !(await fs.pathExists(extractedPath))) {
return Promise.resolve();
}
return fs.chmod(extractedPath, file.externalFileAttributes >>> 16);
}));
}
/**
* Comparison of semantic versions.
*
* Returns a positive number if a is greater than b.
* Returns 0 if a equals b.
* Returns a negative number if a is less than b.
*/
export function versionCompare(a: Version, b: Version): number {
if (a.majorVersion !== b.majorVersion) {
return a.majorVersion - b.majorVersion;
}
if (a.minorVersion !== b.minorVersion) {
return a.minorVersion - b.minorVersion;
}
if (a.patchVersion !== b.patchVersion) {
return a.patchVersion - b.patchVersion;
}
if (a.prereleaseVersion !== undefined && b.prereleaseVersion !== undefined) {
return a.prereleaseVersion.localeCompare(b.prereleaseVersion);
}
return 0;
}
function codeQlLauncherName(): string {
return (os.platform() === "win32") ? "codeql.cmd" : "codeql";
}
function isRedirectStatusCode(statusCode: number): boolean {
return statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308;
}
/*
* Types and helper functions relating to those types.
*/
export enum FindDistributionResultKind {
CompatibleDistribution,
UnknownCompatibilityDistribution,
IncompatibleDistribution,
NoDistribution
}
export type FindDistributionResult = CompatibleDistributionResult | UnknownCompatibilityDistributionResult |
IncompatibleDistributionResult | NoDistributionResult;
interface CompatibleDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.CompatibleDistribution;
version: Version
}
interface UnknownCompatibilityDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.UnknownCompatibilityDistribution;
}
interface IncompatibleDistributionResult {
codeQlPath: string;
kind: FindDistributionResultKind.IncompatibleDistribution;
version: Version;
}
interface NoDistributionResult {
kind: FindDistributionResultKind.NoDistribution;
}
export enum DistributionUpdateCheckResultKind {
AlreadyUpToDate,
InvalidDistributionLocation,
UpdateAvailable
}
type DistributionUpdateCheckResult = DistributionAlreadyUpToDateResult | InvalidDistributionLocationResult |
UpdateAvailableResult;
export interface DistributionAlreadyUpToDateResult {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate;
}
/**
* The distribution could not be installed or updated because it is not managed by the extension.
*/
export interface InvalidDistributionLocationResult {
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation;
}
export interface UpdateAvailableResult {
kind: DistributionUpdateCheckResultKind.UpdateAvailable;
updatedRelease: Release;
}
function createDistributionAlreadyUpToDateResult(): DistributionAlreadyUpToDateResult {
return {
kind: DistributionUpdateCheckResultKind.AlreadyUpToDate
};
}
function createInvalidDistributionLocationResult(): InvalidDistributionLocationResult {
return {
kind: DistributionUpdateCheckResultKind.InvalidDistributionLocation
};
}
function createUpdateAvailableResult(updatedRelease: Release): UpdateAvailableResult {
return {
kind: DistributionUpdateCheckResultKind.UpdateAvailable,
updatedRelease
};
}
/**
* A release on GitHub.
*/
export interface Release {
assets: ReleaseAsset[];
/**
* The creation date of the release on GitHub.
*/
createdAt: string;
/**
* The id associated with the release on GitHub.
*/
id: number;
/**
* The name associated with the release on GitHub.
*/
name: string;
}
/**
* An asset corresponding to a release on GitHub.
*/
export interface ReleaseAsset {
/**
* The id associated with the asset on GitHub.
*/
id: number;
/**
* The name associated with the asset on GitHub.
*/
name: string;
/**
* The size of the asset in bytes.
*/
size: number;
}
/**
* The json returned from github for a release.
*/
export interface GithubRelease {
assets: GithubReleaseAsset[];
/**
* The creation date of the release on GitHub.
*/
created_at: string;
/**
* The id associated with the release on GitHub.
*/
id: number;
/**
* The name associated with the release on GitHub.
*/
name: string;
/**
* Whether the release is a prerelease.
*/
prerelease: boolean;
/**
* The tag name. This should be the version.
*/
tag_name: string;
}
/**
* The json returned by github for an asset in a release.
*/
export interface GithubReleaseAsset {
/**
* The id associated with the asset on GitHub.
*/
id: number;
/**
* The name associated with the asset on GitHub.
*/
name: string;
/**
* The size of the asset in bytes.
*/
size: number;
}
interface VersionConstraint {
description: string;
isVersionCompatible(version: Version): boolean;
}
export class GithubApiError extends Error {
constructor(public status: number, public body: string) {
super(`API call failed with status code ${status}, body: ${body}`);
}
}

View File

@@ -0,0 +1,288 @@
import { commands, Disposable, ExtensionContext, extensions, ProgressLocation, ProgressOptions, window as Window, Uri } from 'vscode';
import { ErrorCodes, LanguageClient, ResponseError } from 'vscode-languageclient';
import * as archiveFilesystemProvider from './archive-filesystem-provider';
import { DistributionConfigListener, QueryServerConfigListener } from './config';
import { DatabaseManager } from './databases';
import { DatabaseUI } from './databases-ui';
import { DistributionUpdateCheckResultKind, DistributionManager, FindDistributionResult, FindDistributionResultKind, GithubApiError, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT } from './distribution';
import * as helpers from './helpers';
import { spawnIdeServer } from './ide-server';
import { InterfaceManager, WebviewReveal } from './interface';
import { ideServerLogger, logger, queryServerLogger } from './logging';
import { compileAndRunQueryAgainstDatabase, EvaluationInfo, tmpDirDisposal, UserCancellationException } from './queries';
import { QueryHistoryItem, QueryHistoryManager } from './query-history';
import * as qsClient from './queryserver-client';
import { CodeQLCliServer } from './cli';
import { assertNever } from './helpers-pure';
/**
* extension.ts
* ------------
*
* A vscode extension for CodeQL query development.
*/
/**
* Holds when we have proceeded past the initial phase of extension activation in which
* we are trying to ensure that a valid CodeQL distribution exists, and we're actually setting
* up the bulk of the extension.
*/
let beganMainExtensionActivation = false;
/**
* A list of vscode-registered-command disposables that contain
* temporary stub handlers for commands that exist package.json (hence
* are already connected to onscreen ui elements) but which will not
* have any useful effect if we haven't located a CodeQL distribution.
*/
const errorStubs: Disposable[] = [];
/**
* Holds when we are installing or checking for updates to the distribution.
*/
let isInstallingOrUpdatingDistribution = false;
/**
* If the user tries to execute vscode commands after extension activation is failed, give
* a sensible error message.
*
* @param excludedCommands List of commands for which we should not register error stubs.
*/
function registerErrorStubs(ctx: ExtensionContext, excludedCommands: string[], stubGenerator: (command: string) => () => void) {
// Remove existing stubs
errorStubs.forEach(stub => stub.dispose());
const extensionId = 'GitHub.vscode-codeql'; // TODO: Is there a better way of obtaining this?
const extension = extensions.getExtension(extensionId);
if (extension === undefined)
throw new Error(`Can't find extension ${extensionId}`);
const stubbedCommands: string[]
= extension.packageJSON.contributes.commands.map((entry: { command: string }) => entry.command);
stubbedCommands.forEach(command => {
if (excludedCommands.indexOf(command) === -1) {
errorStubs.push(commands.registerCommand(command, stubGenerator(command)));
}
});
}
export async function activate(ctx: ExtensionContext): Promise<void> {
// Initialise logging, and ensure all loggers are disposed upon exit.
ctx.subscriptions.push(logger);
logger.log('Starting CodeQL extension');
const distributionConfigListener = new DistributionConfigListener();
ctx.subscriptions.push(distributionConfigListener);
const distributionManager = new DistributionManager(ctx, distributionConfigListener, DEFAULT_DISTRIBUTION_VERSION_CONSTRAINT);
const shouldUpdateOnNextActivationKey = "shouldUpdateOnNextActivation";
registerErrorStubs(ctx, [checkForUpdatesCommand], command => () => {
Window.showErrorMessage(`Can't execute ${command}: waiting to finish loading CodeQL CLI.`);
});
async function installOrUpdateDistributionWithProgressTitle(progressTitle: string, isSilentIfCannotUpdate: boolean): Promise<void> {
const result = await distributionManager.checkForUpdatesToExtensionManagedDistribution();
switch (result.kind) {
case DistributionUpdateCheckResultKind.AlreadyUpToDate:
if (!isSilentIfCannotUpdate) {
helpers.showAndLogInformationMessage("CodeQL CLI already up to date.");
}
break;
case DistributionUpdateCheckResultKind.InvalidDistributionLocation:
if (!isSilentIfCannotUpdate) {
helpers.showAndLogErrorMessage("CodeQL CLI is installed externally so could not be updated.");
}
break;
case DistributionUpdateCheckResultKind.UpdateAvailable:
if (beganMainExtensionActivation) {
const updateAvailableMessage = `Version "${result.updatedRelease.name}" of the CodeQL CLI is now available. ` +
"The update will be installed after Visual Studio Code restarts. Restart now to upgrade?";
ctx.globalState.update(shouldUpdateOnNextActivationKey, true);
if (await helpers.showInformationMessageWithAction(updateAvailableMessage, "Restart and Upgrade")) {
await commands.executeCommand("workbench.action.reloadWindow");
}
} else {
const progressOptions: ProgressOptions = {
location: ProgressLocation.Notification,
title: progressTitle,
cancellable: false,
};
await helpers.withProgress(progressOptions, progress =>
distributionManager.installExtensionManagedDistributionRelease(result.updatedRelease, progress));
ctx.globalState.update(shouldUpdateOnNextActivationKey, false);
helpers.showAndLogInformationMessage(`CodeQL CLI updated to version "${result.updatedRelease.name}".`);
}
break;
default:
assertNever(result);
}
}
async function installOrUpdateDistribution(isSilentIfCannotUpdate: boolean): Promise<void> {
if (isInstallingOrUpdatingDistribution) {
throw new Error("Already installing or updating CodeQL CLI");
}
isInstallingOrUpdatingDistribution = true;
try {
const codeQlInstalled = await distributionManager.getCodeQlPathWithoutVersionCheck() !== undefined;
const messageText = ctx.globalState.get(shouldUpdateOnNextActivationKey) ? "Updating CodeQL CLI" :
codeQlInstalled ? "Checking for updates to CodeQL CLI" : "Installing CodeQL CLI";
await installOrUpdateDistributionWithProgressTitle(messageText, isSilentIfCannotUpdate);
} catch (e) {
// Don't rethrow the exception, because if the config is changed, we want to be able to retry installing
// or updating the distribution.
if (e instanceof GithubApiError && (e.status == 404 || e.status == 403 || e.status === 401)) {
const errorMessageResponse = Window.showErrorMessage("Unable to download CodeQL CLI. See " +
"https://github.com/github/vscode-codeql/blob/master/extensions/ql-vscode/README.md for more details about how " +
"to obtain CodeQL CLI.", "Edit Settings");
// We're deliberately not `await`ing this promise, just
// asynchronously letting the user follow the convenience link
// if they want to.
errorMessageResponse.then(response => {
if (response !== undefined) {
commands.executeCommand('workbench.action.openSettingsJson');
}
});
} else {
helpers.showAndLogErrorMessage("Unable to download CodeQL CLI. " + e);
}
} finally {
isInstallingOrUpdatingDistribution = false;
}
}
async function getDistributionDisplayingDistributionWarnings(): Promise<FindDistributionResult> {
const result = await distributionManager.getDistribution();
switch (result.kind) {
case FindDistributionResultKind.CompatibleDistribution:
logger.log(`Found compatible version of CodeQL CLI (version ${result.version.rawString})`);
break;
case FindDistributionResultKind.IncompatibleDistribution:
helpers.showAndLogWarningMessage("The current version of the CodeQL CLI is incompatible with this extension.");
break;
case FindDistributionResultKind.UnknownCompatibilityDistribution:
helpers.showAndLogWarningMessage("Compatibility with the configured CodeQL CLI could not be determined. " +
"You may experience problems using the extension.");
break;
case FindDistributionResultKind.NoDistribution:
helpers.showAndLogErrorMessage("The CodeQL CLI could not be found.");
break;
default:
assertNever(result);
}
return result;
}
async function installOrUpdateThenTryActivate(isSilentIfCannotUpdate: boolean): Promise<void> {
if (!isInstallingOrUpdatingDistribution) {
await installOrUpdateDistribution(isSilentIfCannotUpdate);
}
// Display the warnings even if the extension has already activated.
const distributionResult = await getDistributionDisplayingDistributionWarnings();
if (!beganMainExtensionActivation && distributionResult.kind !== FindDistributionResultKind.NoDistribution) {
await activateWithInstalledDistribution(ctx, distributionManager);
} else if (distributionResult.kind === FindDistributionResultKind.NoDistribution) {
registerErrorStubs(ctx, [checkForUpdatesCommand], command => async () => {
const installActionName = "Install CodeQL CLI";
const chosenAction = await Window.showErrorMessage(`Can't execute ${command}: missing CodeQL CLI.`, installActionName);
if (chosenAction === installActionName) {
installOrUpdateThenTryActivate(true);
}
});
}
}
ctx.subscriptions.push(distributionConfigListener.onDidChangeDistributionConfiguration(() => installOrUpdateThenTryActivate(true)));
ctx.subscriptions.push(commands.registerCommand(checkForUpdatesCommand, () => installOrUpdateThenTryActivate(false)));
await installOrUpdateThenTryActivate(true);
}
async function activateWithInstalledDistribution(ctx: ExtensionContext, distributionManager: DistributionManager) {
beganMainExtensionActivation = true;
// Remove any error stubs command handlers left over from first part
// of activation.
errorStubs.forEach(stub => stub.dispose());
const qlConfigurationListener = await QueryServerConfigListener.createQueryServerConfigListener(distributionManager);
ctx.subscriptions.push(qlConfigurationListener);
ctx.subscriptions.push(queryServerLogger);
ctx.subscriptions.push(ideServerLogger);
const cliServer = new CodeQLCliServer(distributionManager, logger);
ctx.subscriptions.push(cliServer);
const qs = new qsClient.QueryServerClient(qlConfigurationListener, cliServer, {
logger: queryServerLogger,
}, task => Window.withProgress({ title: 'CodeQL query server', location: ProgressLocation.Window }, task));
ctx.subscriptions.push(qs);
await qs.startQueryServer();
const dbm = new DatabaseManager(ctx, qlConfigurationListener, logger);
ctx.subscriptions.push(dbm);
const databaseUI = new DatabaseUI(ctx, cliServer, dbm, qs);
ctx.subscriptions.push(databaseUI);
const qhm = new QueryHistoryManager(ctx, async item => showResultsForInfo(item.info, WebviewReveal.Forced));
const intm = new InterfaceManager(ctx, dbm, cliServer, queryServerLogger);
ctx.subscriptions.push(intm);
archiveFilesystemProvider.activate(ctx);
async function showResultsForInfo(info: EvaluationInfo, forceReveal: WebviewReveal): Promise<void> {
await intm.showResults(info, forceReveal, false);
}
async function compileAndRunQuery(quickEval: boolean, selectedQuery: Uri | undefined) {
if (qs !== undefined) {
try {
const dbItem = await databaseUI.getDatabaseItem();
if (dbItem === undefined) {
throw new Error('Can\'t run query without a selected database');
}
const info = await compileAndRunQueryAgainstDatabase(cliServer, qs, dbItem, quickEval, selectedQuery);
await showResultsForInfo(info, WebviewReveal.NotForced);
qhm.push(new QueryHistoryItem(info));
}
catch (e) {
if (e instanceof UserCancellationException) {
logger.log(e.message);
}
else if (e instanceof ResponseError && e.code == ErrorCodes.RequestCancelled) {
logger.log(e.message);
}
else if (e instanceof Error)
helpers.showAndLogErrorMessage(e.message);
else
throw e;
}
}
}
ctx.subscriptions.push(tmpDirDisposal);
let client = new LanguageClient('CodeQL Language Server', () => spawnIdeServer(qlConfigurationListener), {
documentSelector: [
{ language: 'ql', scheme: 'file' },
{ language: 'yaml', scheme: 'file', pattern: '**/qlpack.yml' }
],
synchronize: {
configurationSection: 'codeQL'
},
// Ensure that language server exceptions are logged to the same channel as its output.
outputChannel: ideServerLogger.outputChannel
}, true);
ctx.subscriptions.push(commands.registerCommand('codeQL.runQuery', async (uri: Uri | undefined) => await compileAndRunQuery(false, uri)));
ctx.subscriptions.push(commands.registerCommand('codeQL.quickEval', async (uri: Uri | undefined) => await compileAndRunQuery(true, uri)));
ctx.subscriptions.push(client.start());
}
const checkForUpdatesCommand = 'codeQL.checkForUpdatesToCLI';

View File

@@ -0,0 +1,23 @@
/**
* helpers-pure.ts
* ------------
*
* Helper functions that don't depend on vscode and therefore can be used by the front-end and pure unit tests.
*/
/**
* This error is used to indicate a runtime failure of an exhaustivity check enforced at compile time.
*/
class ExhaustivityCheckingError extends Error {
constructor(public expectedExhaustiveValue: never) {
super("Internal error: exhaustivity checking failure");
}
}
/**
* Used to perform compile-time exhaustivity checking on a value. This function will not be executed at runtime unless
* the type system has been subverted.
*/
export function assertNever(value: never): never {
throw new ExhaustivityCheckingError(value);
}

View File

@@ -0,0 +1,136 @@
import * as path from 'path';
import { CancellationToken, ProgressOptions, window as Window, workspace } from 'vscode';
import { logger } from './logging';
import { EvaluationInfo } from './queries';
export interface ProgressUpdate {
/**
* The current step
*/
step: number;
/**
* The maximum step. This *should* be constant for a single job.
*/
maxStep: number;
/**
* The current progress message
*/
message: string;
}
/**
* This mediates between the kind of progress callbacks we want to
* write (where we *set* current progress position and give
* `maxSteps`) and the kind vscode progress api expects us to write
* (which increment progress by a certain amount out of 100%)
*/
export function withProgress<R>(
options: ProgressOptions,
task: (
progress: (p: ProgressUpdate) => void,
token: CancellationToken
) => Thenable<R>
): Thenable<R> {
let progressAchieved = 0;
return Window.withProgress(options,
(progress, token) => {
return task(p => {
const { message, step, maxStep } = p;
const increment = 100 * (step - progressAchieved) / maxStep;
progressAchieved = step;
progress.report({ message, increment });
}, token);
});
}
/**
* Show an error message and log it to the console
*
* @param message — The message to show.
* @param items — A set of items that will be rendered as actions in the message.
*
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogErrorMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showErrorMessage(message, ...items);
}
/**
* Show a warning message and log it to the console
*
* @param message — The message to show.
* @param items — A set of items that will be rendered as actions in the message.
*
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogWarningMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showWarningMessage(message, ...items);
}
/**
* Show an information message and log it to the console
*
* @param message — The message to show.
* @param items — A set of items that will be rendered as actions in the message.
*
* @return — A thenable that resolves to the selected item or undefined when being dismissed.
*/
export function showAndLogInformationMessage(message: string, ...items: string[]): Thenable<string | undefined> {
logger.log(message);
return Window.showInformationMessage(message, ...items);
}
/**
* Opens a modal dialog for the user to make a yes/no choice.
* @param message — The message to show.
*
* @return — `true` if the user clicks 'Yes', `false` if the user clicks 'No' or cancels the dialog.
*/
export async function showBinaryChoiceDialog(message: string): Promise<boolean> {
const yesItem = { title: 'Yes', isCloseAffordance: false };
const noItem = { title: 'No', isCloseAffordance: true }
const chosenItem = await Window.showInformationMessage(message, { modal: true }, yesItem, noItem);
return chosenItem === yesItem;
}
/**
* Show an information message with a customisable action.
* @param message — The message to show.
* @param actionMessage - The call to action message.
*
* @return — `true` if the user clicks the action, `false` if the user cancels the dialog.
*/
export async function showInformationMessageWithAction(message: string, actionMessage: string): Promise<boolean> {
const actionItem = { title: actionMessage, isCloseAffordance: false };
const chosenItem = await Window.showInformationMessage(message, actionItem);
return chosenItem === actionItem;
}
/** Gets all active workspace folders that are on the filesystem. */
export function getOnDiskWorkspaceFolders() {
const workspaceFolders = workspace.workspaceFolders || [];
let diskWorkspaceFolders: string[] = [];
for (const workspaceFolder of workspaceFolders) {
if (workspaceFolder.uri.scheme === "file")
diskWorkspaceFolders.push(workspaceFolder.uri.fsPath)
}
return diskWorkspaceFolders;
}
/**
* Gets a human-readable name for an evaluated query.
* Uses metadata if it exists, and defaults to the query file name.
*/
export function getQueryName(info: EvaluationInfo) {
// Queries run through quick evaluation are not usually the entire query file.
// Label them differently and include the line numbers.
if (info.query.quickEvalPosition !== undefined) {
const { line, endLine, fileName } = info.query.quickEvalPosition;
const lineInfo = line === endLine ? `${line}` : `${line}-${endLine}`;
return `Quick evaluation of ${path.basename(fileName)}:${lineInfo}`;
} else if (info.query.metadata && info.query.metadata.name) {
return info.query.metadata.name;
} else {
return path.basename(info.query.program.queryPath);
}
}

View File

@@ -0,0 +1,26 @@
import { ProgressLocation, window } from 'vscode';
import { StreamInfo } from 'vscode-languageclient';
import * as cli from './cli';
import { QueryServerConfig } from './config';
import { ideServerLogger } from './logging';
/**
* Managing the language server for CodeQL.
*/
/** Starts a new CodeQL language server process, sending progress messages to the status bar. */
export async function spawnIdeServer(config: QueryServerConfig): Promise<StreamInfo> {
return window.withProgress({ title: 'CodeQL language server', location: ProgressLocation.Window }, async (progressReporter, _) => {
const child = cli.spawnServer(
config.codeQlPath,
'CodeQL language server',
['execute', 'language-server'],
['--check-errors', 'ON_CHANGE'],
ideServerLogger,
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
data => ideServerLogger.logWithoutTrailingNewline(data.toString()),
progressReporter
);
return { writer: child.stdin!, reader: child.stdout! };
});
}

View File

@@ -0,0 +1,103 @@
import * as sarif from 'sarif';
import { ResolvableLocationValue } from 'semmle-bqrs';
/**
* Only ever show this many results per run in interpreted results.
*/
export const INTERPRETED_RESULTS_PER_RUN_LIMIT = 100;
/**
* Only ever show this many rows in a raw result table.
*/
export const RAW_RESULTS_LIMIT = 10000;
export interface DatabaseInfo {
name: string;
databaseUri: string;
}
export interface PreviousExecution {
queryName: string;
time: string;
databaseName: string;
durationSeconds: number;
}
export interface Interpretation {
sourceLocationPrefix: string;
numTruncatedResults: number;
sarif: sarif.Log;
}
export interface ResultsInfo {
resultsPath: string;
interpretedResultsPath: string;
}
export interface SortedResultSetInfo {
resultsPath: string;
sortState: SortState;
}
export type SortedResultsMap = { [resultSet: string]: SortedResultSetInfo };
/**
* A message to indicate that the results are being updated.
*
* As a result of receiving this message, listeners might want to display a loading indicator.
*/
export interface ResultsUpdatingMsg {
t: 'resultsUpdating';
}
export interface SetStateMsg {
t: 'setState';
resultsPath: string;
sortedResultsMap: SortedResultsMap;
interpretation: undefined | Interpretation;
database: DatabaseInfo;
kind?: string;
/**
* Whether to keep displaying the old results while rendering the new results.
*
* This is useful to prevent properties like scroll state being lost when rendering the sorted results after sorting a column.
*/
shouldKeepOldResultsWhileRendering: boolean;
};
export type IntoResultsViewMsg = ResultsUpdatingMsg | SetStateMsg;
export type FromResultsViewMsg = ViewSourceFileMsg | ToggleDiagnostics | ChangeSortMsg | ResultViewLoaded;
interface ViewSourceFileMsg {
t: 'viewSourceFile';
loc: ResolvableLocationValue;
databaseUri: string;
};
interface ToggleDiagnostics {
t: 'toggleDiagnostics';
databaseUri: string;
resultsPath: string;
visible: boolean;
kind?: string;
};
interface ResultViewLoaded {
t: 'resultViewLoaded';
};
export enum SortDirection {
asc, desc
}
export interface SortState {
columnIndex: number;
direction: SortDirection;
}
interface ChangeSortMsg {
t: 'changeSort';
resultSetName: string;
sortState?: SortState;
}

View File

@@ -0,0 +1,477 @@
import * as crypto from 'crypto';
import * as path from 'path';
import * as bqrs from 'semmle-bqrs';
import { CustomResultSets, FivePartLocation, LocationStyle, LocationValue, PathProblemQueryResults, ProblemQueryResults, ResolvableLocationValue, tryGetResolvableLocation, WholeFileLocation } from 'semmle-bqrs';
import { FileReader } from 'semmle-io-node';
import { DisposableObject } from 'semmle-vscode-utils';
import * as vscode from 'vscode';
import { Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, languages, Location, Position, Range, Uri, window as Window, workspace } from 'vscode';
import { CodeQLCliServer } from './cli';
import { DatabaseItem, DatabaseManager } from './databases';
import * as helpers from './helpers';
import { showAndLogErrorMessage } from './helpers';
import { assertNever } from './helpers-pure';
import { FromResultsViewMsg, Interpretation, IntoResultsViewMsg, ResultsInfo, SortedResultSetInfo, SortedResultsMap, INTERPRETED_RESULTS_PER_RUN_LIMIT } from './interface-types';
import { Logger } from './logging';
import * as messages from './messages';
import { EvaluationInfo, interpretResults, QueryInfo, tmpDir } from './queries';
/**
* interface.ts
* ------------
*
* Displaying query results and linking back to source files when the
* webview asks us to.
*/
/** Gets a nonce string created with 128 bits of entropy. */
function getNonce(): string {
return crypto.randomBytes(16).toString('base64');
}
/**
* Whether to force webview to reveal
*/
export enum WebviewReveal {
Forced,
NotForced,
}
/**
* Returns HTML to populate the given webview.
* Uses a content security policy that only loads the given script.
*/
function getHtmlForWebview(webview: vscode.Webview, scriptUriOnDisk: vscode.Uri, stylesheetUriOnDisk: vscode.Uri) {
// Convert the on-disk URIs into webview URIs.
const scriptWebviewUri = webview.asWebviewUri(scriptUriOnDisk);
const stylesheetWebviewUri = webview.asWebviewUri(stylesheetUriOnDisk);
// Use a nonce in the content security policy to uniquely identify the above resources.
const nonce = getNonce();
/*
* Content security policy:
* default-src: allow nothing by default.
* script-src: allow only the given script, using the nonce.
* style-src: allow only the given stylesheet, using the nonce.
* connect-src: only allow fetch calls to webview resource URIs
* (this is used to load BQRS result files).
*/
const html = `
<html>
<head>
<meta http-equiv="Content-Security-Policy"
content="default-src 'none'; script-src 'nonce-${nonce}'; style-src 'nonce-${nonce}'; connect-src ${webview.cspSource};">
<link nonce="${nonce}" rel="stylesheet" href="${stylesheetWebviewUri}">
</head>
<body>
<div id=root>
</div>
<script nonce="${nonce}" src="${scriptWebviewUri}">
</script>
</body>
</html>`;
webview.html = html;
}
/** Converts a filesystem URI into a webview URI string that the given panel can use to read the file. */
export function fileUriToWebviewUri(panel: vscode.WebviewPanel, fileUriOnDisk: Uri): string {
return encodeURI(panel.webview.asWebviewUri(fileUriOnDisk).toString(true));
}
/** Converts a URI string received from a webview into a local filesystem URI for the same resource. */
export function webviewUriToFileUri(webviewUri: string): Uri {
// Webview URIs used the vscode-resource scheme. The filesystem path of the resource can be obtained from the path component of the webview URI.
const path = Uri.parse(webviewUri).path;
// For this path to be interpreted on the filesystem, we need to parse it as a filesystem URI for the current platform.
return Uri.file(path);
}
export class InterfaceManager extends DisposableObject {
private _displayedEvaluationInfo?: EvaluationInfo;
private _panel: vscode.WebviewPanel | undefined;
private _panelLoaded = false;
private _panelLoadedCallBacks: (() => void)[] = [];
private readonly _diagnosticCollection = languages.createDiagnosticCollection(`codeql-query-results`);
constructor(public ctx: vscode.ExtensionContext, private databaseManager: DatabaseManager,
public cliServer: CodeQLCliServer, public logger: Logger) {
super();
this.push(this._diagnosticCollection);
}
// Returns the webview panel, creating it if it doesn't already
// exist.
getPanel(): vscode.WebviewPanel {
if (this._panel == undefined) {
const { ctx } = this;
const panel = this._panel = Window.createWebviewPanel(
'resultsView', // internal name
'CodeQL Query Results', // user-visible name
{ viewColumn: vscode.ViewColumn.Beside, preserveFocus: true },
{
enableScripts: true,
enableFindWidget: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.file(tmpDir.name),
vscode.Uri.file(path.join(this.ctx.extensionPath, 'out'))
]
}
);
this._panel.onDidDispose(() => { this._panel = undefined; }, null, ctx.subscriptions);
const scriptPathOnDisk = vscode.Uri
.file(ctx.asAbsolutePath('out/resultsView.js'));
const stylesheetPathOnDisk = vscode.Uri
.file(ctx.asAbsolutePath('out/resultsView.css'));
getHtmlForWebview(panel.webview, scriptPathOnDisk, stylesheetPathOnDisk);
panel.webview.onDidReceiveMessage(async (e) => this.handleMsgFromView(e), undefined, ctx.subscriptions);
}
return this._panel;
}
private async handleMsgFromView(msg: FromResultsViewMsg): Promise<void> {
switch (msg.t) {
case 'viewSourceFile': {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
if (databaseItem !== undefined) {
try {
await showLocation(msg.loc, databaseItem);
}
catch (e) {
if (e instanceof Error) {
if (e.message.match(/File not found/)) {
vscode.window.showErrorMessage(`Original file of this result is not in the database's source archive.`);
}
else {
this.logger.log(`Unable to handleMsgFromView: ${e.message}`);
}
}
else {
this.logger.log(`Unable to handleMsgFromView: ${e}`);
}
}
}
break;
}
case 'toggleDiagnostics': {
if (msg.visible) {
const databaseItem = this.databaseManager.findDatabaseItem(Uri.parse(msg.databaseUri));
if (databaseItem !== undefined) {
await this.showResultsAsDiagnostics(msg.resultsPath, msg.kind, databaseItem);
}
} else {
// TODO: Only clear diagnostics on the same database.
this._diagnosticCollection.clear();
}
break;
}
case "resultViewLoaded":
this._panelLoaded = true;
this._panelLoadedCallBacks.forEach(cb => cb());
this._panelLoadedCallBacks = [];
break;
case 'changeSort': {
if (this._displayedEvaluationInfo === undefined) {
showAndLogErrorMessage("Failed to sort results since evaluation info was unknown.");
break;
}
// Notify the webview that it should expect new results.
await this.postMessage({ t: 'resultsUpdating' });
await this._displayedEvaluationInfo.query.updateSortState(this.cliServer, msg.resultSetName, msg.sortState);
await this.showResults(this._displayedEvaluationInfo, WebviewReveal.NotForced, true);
break;
}
default:
assertNever(msg);
}
}
postMessage(msg: IntoResultsViewMsg): Thenable<boolean> {
return this.getPanel().webview.postMessage(msg);
}
private waitForPanelLoaded(): Promise<void> {
return new Promise((resolve, reject) => {
if (this._panelLoaded) {
resolve();
} else {
this._panelLoadedCallBacks.push(resolve)
}
})
}
/**
* Show query results in webview panel.
* @param info Evaluation info for the executed query.
* @param shouldKeepOldResultsWhileRendering Should keep old results while rendering.
* @param forceReveal Force the webview panel to be visible and
* Appropriate when the user has just performed an explicit
* UI interaction requesting results, e.g. clicking on a query
* history entry.
*/
public async showResults(info: EvaluationInfo, forceReveal: WebviewReveal, shouldKeepOldResultsWhileRendering: boolean = false): Promise<void> {
if (info.result.resultType !== messages.QueryResultType.SUCCESS) {
return;
}
const interpretation = await this.interpretResultsInfo(info.query, info.query.resultsInfo);
const sortedResultsMap: SortedResultsMap = {};
info.query.sortedResultsInfo.forEach((v, k) =>
sortedResultsMap[k] = this.convertPathPropertiesToWebviewUris(v));
this._displayedEvaluationInfo = info;
const panel = this.getPanel();
await this.waitForPanelLoaded();
if (forceReveal === WebviewReveal.Forced) {
panel.reveal(undefined, true);
}
else if (!panel.visible) {
// The results panel exists, (`.getPanel()` guarantees it) but
// is not visible; it's in a not-currently-viewed tab. Show a
// more asynchronous message to not so abruptly interrupt
// user's workflow by immediately revealing the panel.
const showButton = 'View Results';
const queryName = helpers.getQueryName(info);
let queryNameForMessage: string;
if (queryName.length > 0) {
// lower case the first character
queryNameForMessage = queryName.charAt(0).toLowerCase() + queryName.substring(1);
} else {
queryNameForMessage = 'query';
}
const resultPromise = vscode.window.showInformationMessage(
`Finished running ${queryNameForMessage}.`,
showButton
);
// Address this click asynchronously so we still update the
// query history immediately.
resultPromise.then(result => {
if (result === showButton) {
panel.reveal();
}
});
}
await this.postMessage({
t: 'setState',
interpretation,
resultsPath: this.convertPathToWebviewUri(info.query.resultsInfo.resultsPath),
sortedResultsMap,
database: info.database,
shouldKeepOldResultsWhileRendering,
kind: info.query.metadata ? info.query.metadata.kind : undefined
});
}
private async interpretResultsInfo(query: QueryInfo, resultsInfo: ResultsInfo): Promise<Interpretation | undefined> {
let interpretation: Interpretation | undefined = undefined;
if (query.hasInterpretedResults()
&& query.quickEvalPosition === undefined // never do results interpretation if quickEval
) {
try {
const sourceLocationPrefix = await query.dbItem.getSourceLocationPrefix(this.cliServer);
const sourceArchiveUri = query.dbItem.sourceArchive;
const sourceInfo = sourceArchiveUri === undefined ?
undefined :
{ sourceArchive: sourceArchiveUri.fsPath, sourceLocationPrefix };
const sarif = await interpretResults(this.cliServer, query, resultsInfo, sourceInfo);
// For performance reasons, limit the number of results we try
// to serialize and send to the webview. TODO: possibly also
// limit number of paths per result, number of steps per path,
// or throw an error if we are in aggregate trying to send
// massively too much data, as it can make the extension
// unresponsive.
let numTruncatedResults = 0;
sarif.runs.forEach(run => {
if (run.results !== undefined) {
if (run.results.length > INTERPRETED_RESULTS_PER_RUN_LIMIT) {
numTruncatedResults += run.results.length - INTERPRETED_RESULTS_PER_RUN_LIMIT;
run.results = run.results.slice(0, INTERPRETED_RESULTS_PER_RUN_LIMIT);
}
}
});
interpretation = { sarif, sourceLocationPrefix, numTruncatedResults };
}
catch (e) {
// If interpretation fails, accept the error and continue
// trying to render uninterpreted results anyway.
this.logger.log(`Exception during results interpretation: ${e.message}. Will show raw results instead.`);
}
}
return interpretation;
}
private async showResultsAsDiagnostics(resultsPath: string, kind: string | undefined,
database: DatabaseItem) {
// URIs from the webview have the vscode-resource scheme, so convert into a filesystem URI first.
const resultsPathOnDisk = webviewUriToFileUri(resultsPath).fsPath;
const fileReader = await FileReader.open(resultsPathOnDisk);
try {
const resultSets = await bqrs.open(fileReader);
try {
switch (kind || 'problem') {
case 'problem': {
const customResults = bqrs.createCustomResultSets<ProblemQueryResults>(resultSets, ProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
case 'path-problem': {
const customResults = bqrs.createCustomResultSets<PathProblemQueryResults>(resultSets, PathProblemQueryResults);
await this.showProblemResultsAsDiagnostics(customResults, database);
}
break;
default:
throw new Error(`Unrecognized query kind '${kind}'.`);
}
}
catch (e) {
const msg = e instanceof Error ? e.message : e.toString();
this.logger.log(`Exception while computing problem results as diagnostics: ${msg}`);
this._diagnosticCollection.clear();
}
}
finally {
fileReader.dispose();
}
}
private async showProblemResultsAsDiagnostics(results: CustomResultSets<ProblemQueryResults>,
databaseItem: DatabaseItem): Promise<void> {
const diagnostics: [Uri, ReadonlyArray<Diagnostic>][] = [];
for await (const problemRow of results.problems.readTuples()) {
const codeLocation = resolveLocation(problemRow.element.location, databaseItem);
let message: string;
const references = problemRow.references;
if (references) {
let referenceIndex = 0;
message = problemRow.message.replace(/\$\@/g, sub => {
if (referenceIndex < references.length) {
const replacement = references[referenceIndex].text;
referenceIndex++;
return replacement;
}
else {
return sub;
}
});
}
else {
message = problemRow.message;
}
const diagnostic = new Diagnostic(codeLocation.range, message, DiagnosticSeverity.Warning);
if (problemRow.references) {
const relatedInformation: DiagnosticRelatedInformation[] = [];
for (const reference of problemRow.references) {
const referenceLocation = tryResolveLocation(reference.element.location, databaseItem);
if (referenceLocation) {
const related = new DiagnosticRelatedInformation(referenceLocation,
reference.text);
relatedInformation.push(related);
}
}
diagnostic.relatedInformation = relatedInformation;
}
diagnostics.push([
codeLocation.uri,
[diagnostic]
]);
}
this._diagnosticCollection.set(diagnostics);
}
private convertPathToWebviewUri(path: string): string {
return fileUriToWebviewUri(this.getPanel(), Uri.file(path));
}
private convertPathPropertiesToWebviewUris(info: SortedResultSetInfo): SortedResultSetInfo {
return {
resultsPath: this.convertPathToWebviewUri(info.resultsPath),
sortState: info.sortState
};
}
}
async function showLocation(loc: ResolvableLocationValue, databaseItem: DatabaseItem): Promise<void> {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
const doc = await workspace.openTextDocument(resolvedLocation.uri);
const editor = await Window.showTextDocument(doc, vscode.ViewColumn.One);
const sel = new vscode.Selection(resolvedLocation.range.start, resolvedLocation.range.end);
editor.selection = sel;
editor.revealRange(sel, vscode.TextEditorRevealType.InCenter);
}
}
/**
* Resolves the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the file location.
*/
function resolveFivePartLocation(loc: FivePartLocation, databaseItem: DatabaseItem): Location {
// `Range` is a half-open interval, and is zero-based. CodeQL locations are closed intervals, and
// are one-based. Adjust accordingly.
const range = new Range(Math.max(0, loc.lineStart - 1),
Math.max(0, loc.colStart - 1),
Math.max(0, loc.lineEnd - 1),
Math.max(0, loc.colEnd));
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolves the specified CodeQL filesystem resource location to a URI into the source archive.
* @param loc CodeQL location to resolve, corresponding to an entire filesystem resource. Must have a non-empty value for `loc.file`.
* @param databaseItem Database in which to resolve the filesystem resource location.
*/
function resolveWholeFileLocation(loc: WholeFileLocation, databaseItem: DatabaseItem): Location {
// A location corresponding to the start of the file.
const range = new Range(0, 0, 0, 0);
return new Location(databaseItem.resolveSourceFile(loc.file), range);
}
/**
* Resolve the specified CodeQL location to a URI into the source archive.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
function resolveLocation(loc: LocationValue | undefined, databaseItem: DatabaseItem): Location {
const resolvedLocation = tryResolveLocation(loc, databaseItem);
if (resolvedLocation) {
return resolvedLocation;
}
else {
// Return a fake position in the source archive directory itself.
return new Location(databaseItem.resolveSourceFile(undefined), new Position(0, 0));
}
}
/**
* Try to resolve the specified CodeQL location to a URI into the source archive. If no exact location
* can be resolved, returns `undefined`.
* @param loc CodeQL location to resolve
* @param databaseItem Database in which to resolve the file location.
*/
function tryResolveLocation(loc: LocationValue | undefined,
databaseItem: DatabaseItem): Location | undefined {
const resolvableLoc = tryGetResolvableLocation(loc);
if (resolvableLoc === undefined) {
return undefined;
}
switch (resolvableLoc.t) {
case LocationStyle.FivePart:
return resolveFivePartLocation(resolvableLoc, databaseItem);
case LocationStyle.WholeFile:
return resolveWholeFileLocation(resolvableLoc, databaseItem);
default:
return undefined;
}
}

View File

@@ -0,0 +1,40 @@
import { window as Window, OutputChannel, Progress } from 'vscode';
import { DisposableObject } from 'semmle-vscode-utils';
export interface Logger {
/** Writes the given log message, followed by a newline. */
log(message: string): void;
/** Writes the given log message, not followed by a newline. */
logWithoutTrailingNewline(message: string): void;
}
export type ProgressReporter = Progress<{ message: string }>;
/** A logger that writes messages to an output channel in the Output tab. */
export class OutputChannelLogger extends DisposableObject implements Logger {
outputChannel: OutputChannel;
constructor(title: string) {
super();
this.outputChannel = Window.createOutputChannel(title);
this.push(this.outputChannel);
}
log(message: string) {
this.outputChannel.appendLine(message);
}
logWithoutTrailingNewline(message: string) {
this.outputChannel.append(message);
}
}
/** The global logger for the extension. */
export const logger = new OutputChannelLogger('CodeQL Extension Log');
/** The logger for messages from the query server. */
export const queryServerLogger = new OutputChannelLogger('CodeQL Query Server');
/** The logger for messages from the language server. */
export const ideServerLogger = new OutputChannelLogger('CodeQL Language Server');

View File

@@ -0,0 +1,918 @@
/**
* Types for messages exchanged during jsonrpc communication with the
* the CodeQL query server.
*/
import * as rpc from 'vscode-jsonrpc';
/**
* A position within a QL file.
*/
export interface Position {
/**
* The one-based index of the start line
*/
line: number;
/**
* The one-based offset of the start column within
* the start line in UTF-16 code-units
*/
column: number;
/**
* The one-based index of the end line line
*/
endLine: number;
/**
* The one-based offset of the end column within
* the end line in UTF-16 code-units
*/
endColumn: number;
/**
* The path of the file.
* If the file name is "Compiler Generated" the
* the position is not a real position but
* arises from compiler generated code.
*/
fileName: string;
}
/**
* A query that should be checked for any errors or warnings
*/
export interface CheckQueryParams {
/**
* The options for compilation, if missing then the default options.
*/
compilationOptions?: CompilationOptions;
/**
* The ql program to check.
*/
queryToCheck: QlProgram;
/**
* The way of compiling a query
*/
target: CompilationTarget;
}
/**
* A query that should compiled into a qlo
*/
export interface CompileQueryParams {
/**
* The options for compilation, if missing then the default options.
*/
compilationOptions?: CompilationOptions;
/**
* The options for compilation that do not affect the result.
*/
extraOptions?: ExtraOptions;
/**
* The ql program to check.
*/
queryToCheck: QlProgram;
/**
* The way of compiling a query
*/
target: CompilationTarget;
/**
* The path to write the qlo at.
*/
resultPath?: string;
}
/**
* A dil (datalog intermediate language) query that should compiled into a qlo
*/
export interface CompileDilParams {
/**
* The options for compilation, if missing then the default options.
*/
compilationOptions?: DilCompilationOptions;
/**
* The options for compilation that do not affect the result.
*/
extraOptions?: ExtraOptions;
/**
* The dil query to compile
*/
dilQuery?: DILQuery;
/**
* The path to write the qlo at.
*/
resultPath?: string;
}
/**
* The options for QL compilation.
*/
export interface CompilationOptions {
/**
* Whether to ensure that elements that do not have a location or URL
* get a default location.
*/
computeNoLocationUrls: boolean;
/**
* Whether to fail if any warnings occur in the ql code.
*/
failOnWarnings: boolean;
/**
* Whether to compile as fast as possible, at the expense
* of optimization.
*/
fastCompilation: boolean;
/**
* Whether to include dil within qlos.
*/
includeDilInQlo: boolean;
/**
* Whether to only do the initial program checks on the subset of the program that
* is used.
*/
localChecking: boolean;
/**
* Whether to disable urls in the results.
*/
noComputeGetUrl: boolean;
/**
* Whether to disable toString values in the results.
*/
noComputeToString: boolean;
}
/**
* Compilation options that do not affect the result of
* query compilation
*/
export interface ExtraOptions {
/**
* The uris of any additional compilation caches
* TODO: Document cache uri format
*/
extraCompilationCache?: string;
/**
* The compilation timeout in seconds. If it is
* zero then there is no timeout.
*/
timeoutSecs: number;
}
/**
* The DIL compilation options
*/
export interface DilCompilationOptions {
/**
* Whether to compile as fast as possible, at the expense
* of optimization.
*/
fastCompilation: boolean;
/**
* Whether to include dil within qlos.
*/
includeDilInQlo: boolean;
}
/**
* A full ql program
*/
export interface QlProgram {
/**
* The path to the dbscheme
*/
dbschemePath: string;
/**
*The ql library search path
*/
libraryPath: string[];
/**
* The path to the query
*/
queryPath: string;
/**
* If set then the contents of the source files.
* Otherwise they will be searched for on disk.
*/
sourceContents?: QlFileSet;
}
/**
* A representation of files in query with all imports
* pre-resolved.
*/
export interface QlFileSet {
/**
* The files imported by the given file
*/
imports: { [key: string]: string[]; };
/**
* An id of each file
*/
nodeNumbering: { [key: string]: number; };
/**
* The code for each file
*/
qlCode: { [key: string]: string; };
/**
* The resolution of an import in each directory.
*/
resolvedDirImports: { [key: string]: { [key: string]: string; }; };
}
/**
* An uncompiled dil query
*/
export interface DILQuery {
/**
* The path to the dbscheme
*/
dbschemePath: string;
/**
* The path to the dil file
*/
dilPath: string;
/**
* The dil source
*/
dilSource: string;
}
/**
* The way of compiling the query, as a normal query
* or a subset of it. Note that precisely one of the two options should be set.
*/
export interface CompilationTarget {
/**
* Compile as a normal query
*/
query?: {};
/**
* Compile as a quick evaluation
*/
quickEval?: QuickEvalOptions;
}
/**
* Options for quick evaluation
*/
export interface QuickEvalOptions {
quickEvalPos?: Position;
}
/**
* The result of checking a query.
*/
export interface CheckQueryResult {
/**
* Whether the query came from a compilation cache
*/
fromCache: boolean;
/**
* The errors or warnings that occurred during compilation
*/
messages: CompilationMessage[];
/**
* The types of the query predicates of the query
*/
resultPatterns: ResultPattern[];
}
/**
* A compilation message (either an error or a warning)
*/
export interface CompilationMessage {
/**
* The text of the message
*/
message: string;
/**
* The source position associated with the message
*/
position: Position;
/**
* The severity of the message
*/
severity: number;
}
/**
* Severity of different messages
*/
export namespace Severity {
/**
* The message is a compilation error.
*/
export const ERROR = 0;
/**
* The message is a compilation warning.
*/
export const WARNING = 1;
}
/**
* The type of a query predicate
*/
export interface ResultPattern {
/**
* The types of the columns of the query predicate
*/
columns: ResultColumn[];
/**
* The name of the query predicate.
* #select" is used as the name of a select clause.
*/
name: string;
}
/**
* The name and type of a single column
*/
export interface ResultColumn {
/**
* The kind of the column. See `ResultColumnKind`
* for the current possible meanings
*/
kind: number;
/**
* The name of the column.
* This may be compiler generated for complex select expressions.
*/
name: string;
}
/**
* The kind of a result column.
*/
export namespace ResultColumnKind {
/**
* A column of type `float`
*/
export const FLOAT = 0;
/**
* A column of type `int`
*/
export const INTEGER = 1;
/**
* A column of type `string`
*/
export const STRING = 2;
/**
* A column of type `boolean`
*/
export const BOOLEAN = 3;
/**
* A column of type `date`
*/
export const DATE = 4;
/**
* A column of a non-primitive type
*/
export const ENTITY = 5;
}
/**
* Parameters for compiling an upgrade.
*/
export interface CompileUpgradeParams {
/**
* The parameters for how to compile the upgrades
*/
upgrade: UpgradeParams;
/**
* A directory to store parts of the compiled upgrade
*/
upgradeTempDir: string;
}
/**
* Parameters describing an upgrade
*/
export interface UpgradeParams {
/**
* The location of non built-in upgrades
*/
additionalUpgrades: string[];
/**
* The path to the current dbscheme to start the upgrade
*/
fromDbscheme: string;
/**
* The path to the target dbscheme to try an upgrade to
*/
toDbscheme: string;
}
/**
* The result of checking an upgrade
*/
export interface CheckUpgradeResult {
/**
* A description of the steps to take to upgrade this dataset.
* Note that the list may be partial.
*/
checkedUpgrades?: UpgradesDescription;
/**
* Any errors that occurred when checking the scripts.
*/
upgradeError?: string;
}
/**
* The result of compiling an upgrade
*/
export interface CompileUpgradeResult {
/**
* The compiled upgrade.
*/
compiledUpgrades?: CompiledUpgrades;
/**
* Any errors that occurred when checking the scripts.
*/
error?: string;
}
/**
* A description of a upgrade process
*/
export interface UpgradesDescription {
/**
* The initial sha of the dbscheme to upgrade from
*/
initialSha: string;
/**
* A list of description of the steps in the upgrade process.
* Note that this may only upgrade partially
*/
scripts: UpgradeDescription[];
/**
* The sha of the target dataset.
*/
targetSha: string;
}
/**
* The description of a single step
*/
export interface UpgradeDescription {
/**
* The compatibility of the upgrade
*/
compatibility: string;
/**
* A description of the upgrade
*/
description: string;
/**
* The dbscheme sha after this upgrade has run.
*/
newSha: string;
}
/**
* A compiled upgrade.
*/
export interface CompiledUpgrades {
/**
* The initial sha of the dbscheme to upgrade from
*/
initialSha: string;
/**
* The path to the new dataset statistics
*/
newStatsPath: string;
/**
* The steps in the upgrade path
*/
scripts: CompiledUpgradeScript[];
/**
* The sha of the target dataset.
*/
targetSha: string;
}
/**
* A compiled step to upgrade the dataset.
*/
export interface CompiledUpgradeScript {
/**
* A description of the spec
*/
description: UpgradeDescription;
/**
* The path to the dbscheme that this upgrade step
* upgrades to.
*/
newDbschemePath: string;
/**
* The actions required to run this step.
*/
specs: UpgradeAction[];
}
/**
* An action used to upgrade a query.
* Only one of the options should be set
*/
export interface UpgradeAction {
deleted?: DeleteSpec;
runQuery?: QloSpec;
}
/**
* Delete a relation
*/
export interface DeleteSpec {
/**
* The name of the relation to delete
*/
relationToDelete: string;
}
/**
* Run a qlo to provide a relation
*/
export interface QloSpec {
/**
* The name of the relation to create/replace
*/
newRelation: string;
/**
* The Uri of the qlo to run
*/
qloUri: string;
}
/**
* Parameters to clear the cache
*/
export interface ClearCacheParams {
/**
* The dataset for which we want to clear the cache
*/
db: Dataset;
/**
* Whether the cache should actually be cleared.
*/
dryRun: boolean;
}
/**
* Parameters for trimming the cache of a dataset
*/
export interface TrimCacheParams {
/**
* The dataset that we want to trim the cache of.
*/
db: Dataset;
}
/**
* A ql dataset
*/
export interface Dataset {
/**
* The path to the dataset
*/
dbDir: string;
/**
* The name of the working set (normally "default")
*/
workingSet: string;
}
/**
* The result of trimming or clearing the cache.
*/
export interface ClearCacheResult {
/**
* A user friendly message saying what was or would be
* deleted.
*/
deletionMessage: string;
}
/**
* Parameters for running a set of queries
*/
export interface EvaluateQueriesParams {
/**
* The dataset to run on
*/
db: Dataset;
/**
* An identifier used in callbacks to identify this run.
*/
evaluateId: number;
/**
* The queries to run
*/
queries: QueryToRun[];
/**
* Whether the evaluator should stop on a non fatal-error
*/
stopOnError: boolean;
/**
* Whether the evaluator should assume this is the final
* run on this dataset before it's cache would be deleted.
*/
useSequenceHint: boolean;
}
/**
* A single query that should be run
*/
export interface QueryToRun {
/**
* The id of this query within the run
*/
id: number;
/**
* A uri pointing to the qlo to run.
*/
qlo: string;
/**
* The path where we should save this queries results
*/
resultsPath: string;
/**
* The per stage timeout (0 for no timeout)
*/
timeoutSecs: number;
/**
* Values to set for each template
*/
templateValues?: { [key: string]: TemplateSource; };
/**
* Whether templates without values in the templateValues
* map should be set to the empty set or give an error.
*/
allowUnknownTemplates: boolean;
}
/**
* The source of templates. Only one
*/
export interface TemplateSource {
/**
* Do basic interpretation of query results and
* use the interpreted results in the query.
* This should only be used to support legacy filter
* queries.
*/
interpretedInput?: ProblemResults;
/**
* Use the explicitly listed values
*/
values?: RelationValues;
}
/**
* A relation as a list of tuples
*/
export interface RelationValues {
tuples: Value[][];
}
/**
* A single primitive value for templates.
* Only one case should be set.
*/
export interface Value {
booleanValue?: boolean;
dateValue?: string;
doubleValue?: number;
intValue?: number;
stringValue?: string;
}
/**
* A relation made by interpreting the results of a problem or metric query
* to be used as the input to a filter query.
*/
export interface ProblemResults {
/**
* The path to the original query.
*/
queryPath: string;
/**
* The way of obtaining the queries
*/
results: ResultSet;
/**
* Whether the results are for a defect filter or a metric filter.
*/
type: ResultType;
}
/**
* The type of results that are going to be sent into the filter query.
*/
export enum ResultType {
METRIC = 0,
DEFECT = 1,
}
/**
* The way of obtaining the results
*/
export interface ResultSet {
/**
* Via an earlier query in the evaluation run
*/
precedingQuery?: number;
/**
* Directly from an existing results set.
*/
resultsFile?: string;
}
/**
* The type returned when the evaluation is complete
*/
export interface EvaluationComplete { }
/**
* The result of a single query
*/
export interface EvaluationResult {
/**
* The id of the run that this query was in
*/
runId: number;
/**
* The id of the query within the run
*/
queryId: number;
/**
* The type of the result. See QueryResultType for
* possible meanings. Any other result should be interpreted as an error.
*/
resultType: number;
/**
* The wall clock time it took to evaluate the query.
* The time is from when we initially tried to evaluate the query
* to when we get the results. Hence with parallel evaluation the times may
* look odd.
*/
evaluationTime: number;
/**
* An error message if an error happened
*/
message?: string;
}
/**
* The result of running a query,
*/
export namespace QueryResultType {
/**
* The query ran successfully
*/
export const SUCCESS = 0;
/**
* The query failed due to an reason
* that isn't listed
*/
export const OTHER_ERROR = 1;
/**
* The query failed due to running out of
* memory
*/
export const OOM = 2;
/**
* The query failed due to exceeding the timeout
*/
export const TIMEOUT = 3;
/**
* The query failed because it was cancelled.
*/
export const CANCELLATION = 4;
}
/**
* Parameters for running an upgrade
*/
export interface RunUpgradeParams {
/**
* The dataset to upgrade
*/
db: Dataset;
/**
* The per stage timeout in seconds. Use 0
* for no timeout.
*/
timeoutSecs: number;
/**
* The upgrade to run
*/
toRun: CompiledUpgrades;
}
/**
* The result of running an upgrade
*/
export interface RunUpgradeResult {
/**
* The type of the result. See QueryResultType for
* possible meanings. Any other result should be interpreted as an error.
*/
resultType: number;
/**
* The error message if an error occurred
*/
error?: string;
/**
* The new dbscheme sha.
*/
finalSha: string;
}
/**
* Type for any action that could have progress messages.
*/
export interface WithProgressId<T> {
/**
* The main body
*/
body: T,
/**
* The id used to report progress updates
*/
progressId: number
}
export interface ProgressMessage {
/**
* The id of the operation that is running
*/
id: number;
/**
* The current step
*/
step: number;
/**
* The maximum step. This *should* be constant for a single job.
*/
maxStep: number;
/**
* The current progress message
*/
message: string;
}
/**
* Check a Ql query for errors without compiling it
*/
export const checkQuery = new rpc.RequestType<WithProgressId<CheckQueryParams>, CheckQueryResult, void, void>('compilation/checkQuery');
/**
* Compile a Ql query into a qlo
*/
export const compileQuery = new rpc.RequestType<WithProgressId<CompileQueryParams>, CheckQueryResult, void, void>('compilation/compileQuery');
/**
* Compile a dil query into a qlo
*/
export const compileDilQuery = new rpc.RequestType<WithProgressId<CompileDilParams>, CheckQueryResult, void, void>('compilation/compileDilQuery');
/**
* Check if there is a valid upgrade path between two dbschemes.
*/
export const checkUpgrade = new rpc.RequestType<WithProgressId<UpgradeParams>, CheckUpgradeResult, void, void>('compilation/checkUpgrade');
/**
* Compile an upgrade script to upgrade a dataset.
*/
export const compileUpgrade = new rpc.RequestType<WithProgressId<CompileUpgradeParams>, CompileUpgradeResult, void, void>('compilation/compileUpgrade');
/**
* Clear the cache of a dataset
*/
export const clearCache = new rpc.RequestType<WithProgressId<ClearCacheParams>, ClearCacheResult, void, void>('evaluation/clearCache');
/**
* Trim the cache of a dataset
*/
export const trimCache = new rpc.RequestType<WithProgressId<TrimCacheParams>, ClearCacheResult, void, void>('evaluation/trimCache');
/**
* Run some queries on a dataset
*/
export const runQueries = new rpc.RequestType<WithProgressId<EvaluateQueriesParams>, EvaluationComplete, void, void>('evaluation/runQueries');
/**
* Run upgrades on a dataset
*/
export const runUpgrade = new rpc.RequestType<WithProgressId<RunUpgradeParams>, RunUpgradeResult, void, void>('evaluation/runUpgrade');
/**
* Request returned to the client to notify completion of a query.
* The full runQueries job is completed when all queries are acknowledged.
*/
export const completeQuery = new rpc.RequestType<EvaluationResult, Object, void, void>('evaluation/queryCompleted');
/**
* A notification that the progress has been changed.
*/
export const progress = new rpc.NotificationType<ProgressMessage, void>('ql/progressUpdated');

View File

@@ -0,0 +1,663 @@
import * as crypto from 'crypto';
import * as fs from 'fs-extra';
import * as path from 'path';
import * as sarif from 'sarif';
import * as tmp from 'tmp';
import * as vscode from 'vscode';
import * as cli from './cli';
import { DatabaseItem } from './databases';
import * as helpers from './helpers';
import { DatabaseInfo, SortState, ResultsInfo, SortedResultSetInfo } from './interface-types';
import { logger } from './logging';
import * as messages from './messages';
import * as qsClient from './queryserver-client';
import { promisify } from 'util';
/**
* queries.ts
* -------------
*
* Compiling and running QL queries.
*/
// XXX: Tmp directory should be configuarble.
export const tmpDir = tmp.dirSync({ prefix: 'queries_', keep: false, unsafeCleanup: true });
const upgradesTmpDir = tmp.dirSync({ dir: tmpDir.name, prefix: 'upgrades_', keep: false, unsafeCleanup: true });
export const tmpDirDisposal = {
dispose: () => {
upgradesTmpDir.removeCallback();
tmpDir.removeCallback();
}
};
let queryCounter = 0;
export class UserCancellationException extends Error { }
/**
* A collection of evaluation-time information about a query,
* including the query itself, and where we have decided to put
* temporary files associated with it, such as the compiled query
* output and results.
*/
export class QueryInfo {
compiledQueryPath: string;
resultsInfo: ResultsInfo;
/**
* Map from result set name to SortedResultSetInfo.
*/
sortedResultsInfo: Map<string, SortedResultSetInfo>;
dataset: vscode.Uri; // guarantee the existence of a well-defined dataset dir at this point
constructor(
public program: messages.QlProgram,
public dbItem: DatabaseItem,
public queryDbscheme: string, // the dbscheme file the query expects, based on library path resolution
public quickEvalPosition?: messages.Position,
public metadata?: cli.QueryMetadata,
) {
this.compiledQueryPath = path.join(tmpDir.name, `compiledQuery${queryCounter}.qlo`);
this.resultsInfo = {
resultsPath: path.join(tmpDir.name, `results${queryCounter}.bqrs`),
interpretedResultsPath: path.join(tmpDir.name, `interpretedResults${queryCounter}.sarif`)
};
this.sortedResultsInfo = new Map();
if (dbItem.contents === undefined) {
throw new Error('Can\'t run query on invalid database.');
}
this.dataset = dbItem.contents.datasetUri;
queryCounter++;
}
async run(
qs: qsClient.QueryServerClient,
): Promise<messages.EvaluationResult> {
let result: messages.EvaluationResult | null = null;
const callbackId = qs.registerCallback(res => { result = res });
const queryToRun: messages.QueryToRun = {
resultsPath: this.resultsInfo.resultsPath,
qlo: vscode.Uri.file(this.compiledQueryPath).toString(),
allowUnknownTemplates: true,
id: callbackId,
timeoutSecs: qs.config.timeoutSecs,
}
const dataset: messages.Dataset = {
dbDir: this.dataset.fsPath,
workingSet: 'default'
}
const params: messages.EvaluateQueriesParams = {
db: dataset,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
useSequenceHint: false
}
try {
await helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running Query",
cancellable: true,
}, (progress, token) => {
return qs.sendRequest(messages.runQueries, params, token, progress)
});
} finally {
qs.unRegisterCallback(callbackId);
}
return result || { evaluationTime: 0, message: "No result from server", queryId: -1, runId: callbackId, resultType: messages.QueryResultType.OTHER_ERROR };
}
async compile(
qs: qsClient.QueryServerClient,
): Promise<messages.CompilationMessage[]> {
let compiled: messages.CheckQueryResult;
try {
const params: messages.CompileQueryParams = {
compilationOptions: {
computeNoLocationUrls: true,
failOnWarnings: false,
fastCompilation: false,
includeDilInQlo: true,
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
},
extraOptions: {
timeoutSecs: qs.config.timeoutSecs
},
queryToCheck: this.program,
resultPath: this.compiledQueryPath,
target: !!this.quickEvalPosition ? { quickEval: { quickEvalPos: this.quickEvalPosition } } : { query: {} }
};
compiled = await helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling Query",
cancellable: true,
}, (progress, token) => {
return qs.sendRequest(messages.compileQuery, params, token, progress);
});
} finally {
qs.logger.log(" - - - COMPILATION DONE - - - ");
}
return (compiled.messages || []).filter(msg => msg.severity == 0);
}
/**
* Holds if this query should produce interpreted results.
*/
hasInterpretedResults(): boolean {
return this.dbItem.hasDbInfo();
}
async updateSortState(server: cli.CodeQLCliServer, resultSetName: string, sortState: SortState | undefined): Promise<void> {
if (sortState === undefined) {
this.sortedResultsInfo.delete(resultSetName);
return;
}
const sortedResultSetInfo: SortedResultSetInfo = {
resultsPath: path.join(tmpDir.name, `sortedResults${queryCounter}-${resultSetName}.bqrs`),
sortState
};
await server.sortBqrs(this.resultsInfo.resultsPath, sortedResultSetInfo.resultsPath, resultSetName, [sortState.columnIndex], [sortState.direction]);
this.sortedResultsInfo.set(resultSetName, sortedResultSetInfo);
}
}
/**
* Call cli command to interpret results.
*/
export async function interpretResults(server: cli.CodeQLCliServer, queryInfo: QueryInfo, resultsInfo: ResultsInfo, sourceInfo?: cli.SourceInfo): Promise<sarif.Log> {
if (await fs.pathExists(resultsInfo.interpretedResultsPath)) {
return JSON.parse(await fs.readFile(resultsInfo.interpretedResultsPath, 'utf8'));
}
const { metadata } = queryInfo;
if (metadata == undefined) {
throw new Error('Can\'t interpret results without query metadata');
}
let { kind, id } = metadata;
if (kind == undefined) {
throw new Error('Can\'t interpret results without query metadata including kind');
}
if (id == undefined) {
// Interpretation per se doesn't really require an id, but the
// SARIF format does, so in the absence of one, we invent one
// based on the query path.
//
// Just to be careful, sanitize to remove '/' since SARIF (section
// 3.27.5 "ruleId property") says that it has special meaning.
id = queryInfo.program.queryPath.replace(/\//g, '-');
}
return await server.interpretBqrs({ kind, id }, resultsInfo.resultsPath, resultsInfo.interpretedResultsPath, sourceInfo);
}
export interface EvaluationInfo {
query: QueryInfo;
result: messages.EvaluationResult;
database: DatabaseInfo;
}
/**
* Checks whether the given database can be upgraded to the given target DB scheme,
* and whether the user wants to proceed with the upgrade.
* Reports errors to both the user and the console.
* @returns the `UpgradeParams` needed to start the upgrade, if the upgrade is possible and was confirmed by the user, or `undefined` otherwise.
*/
async function checkAndConfirmDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.UpgradeParams | undefined> {
if (db.contents === undefined || db.contents.dbSchemeUri === undefined) {
helpers.showAndLogErrorMessage("Database is invalid, and cannot be upgraded.");
return;
}
const params: messages.UpgradeParams = {
fromDbscheme: db.contents.dbSchemeUri.fsPath,
toDbscheme: targetDbScheme.fsPath,
additionalUpgrades: upgradesDirectories.map(uri => uri.fsPath)
};
let checkUpgradeResult: messages.CheckUpgradeResult;
try {
qs.logger.log('Checking database upgrade...');
checkUpgradeResult = await checkDatabaseUpgrade(qs, params);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${e}`);
return;
}
finally {
qs.logger.log('Done checking database upgrade.');
}
const checkedUpgrades = checkUpgradeResult.checkedUpgrades;
if (checkedUpgrades === undefined) {
const error = checkUpgradeResult.upgradeError || '[no error message available]';
await helpers.showAndLogErrorMessage(`Database cannot be upgraded: ${error}`);
return;
}
if (checkedUpgrades.scripts.length === 0) {
await helpers.showAndLogInformationMessage('Database is already up to date; nothing to do.');
return;
}
let curSha = checkedUpgrades.initialSha;
let descriptionMessage = '';
for (const script of checkedUpgrades.scripts) {
descriptionMessage += `Would perform upgrade: ${script.description}\n`;
descriptionMessage += `\t-> Compatibility: ${script.compatibility}\n`;
curSha = script.newSha;
}
const targetSha = checkedUpgrades.targetSha;
if (curSha != targetSha) {
// Newlines aren't rendered in notifications: https://github.com/microsoft/vscode/issues/48900
// A modal dialog would be rendered better, but is more intrusive.
await helpers.showAndLogErrorMessage(`Database cannot be upgraded to the target database scheme.
Can upgrade from ${checkedUpgrades.initialSha} (current) to ${curSha}, but cannot reach ${targetSha} (target).`);
// TODO: give a more informative message if we think the DB is ahead of the target DB scheme
return;
}
logger.log(descriptionMessage);
// Ask the user to confirm the upgrade.
const shouldUpgrade = await helpers.showBinaryChoiceDialog(`Should the database ${db.databaseUri.fsPath} be upgraded?\n\n${descriptionMessage}`);
if (shouldUpgrade) {
return params;
}
else {
throw new UserCancellationException('User cancelled the database upgrade.');
}
}
/**
* Command handler for 'Upgrade Database'.
* Attempts to upgrade the given database to the given target DB scheme, using the given directory of upgrades.
* First performs a dry-run and prompts the user to confirm the upgrade.
* Reports errors during compilation and evaluation of upgrades to the user.
*/
export async function upgradeDatabase(qs: qsClient.QueryServerClient, db: DatabaseItem, targetDbScheme: vscode.Uri, upgradesDirectories: vscode.Uri[]):
Promise<messages.RunUpgradeResult | undefined> {
const upgradeParams = await checkAndConfirmDatabaseUpgrade(qs, db, targetDbScheme, upgradesDirectories);
if (upgradeParams === undefined) {
return;
}
let compileUpgradeResult: messages.CompileUpgradeResult;
try {
compileUpgradeResult = await compileDatabaseUpgrade(qs, upgradeParams);
}
catch (e) {
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${e}`);
return;
}
finally {
qs.logger.log('Done compiling database upgrade.')
}
if (compileUpgradeResult.compiledUpgrades === undefined) {
const error = compileUpgradeResult.error || '[no error message available]';
helpers.showAndLogErrorMessage(`Compilation of database upgrades failed: ${error}`);
return;
}
try {
qs.logger.log('Running the following database upgrade:');
qs.logger.log(compileUpgradeResult.compiledUpgrades.scripts.map(s => s.description.description).join('\n'));
return await runDatabaseUpgrade(qs, db, compileUpgradeResult.compiledUpgrades);
}
catch (e) {
helpers.showAndLogErrorMessage(`Database upgrade failed: ${e}`);
return;
}
finally {
qs.logger.log('Done running database upgrade.')
}
}
async function checkDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CheckUpgradeResult> {
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Checking for database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.checkUpgrade, upgradeParams, token, progress));
}
async function compileDatabaseUpgrade(qs: qsClient.QueryServerClient, upgradeParams: messages.UpgradeParams):
Promise<messages.CompileUpgradeResult> {
const params: messages.CompileUpgradeParams = {
upgrade: upgradeParams,
upgradeTempDir: upgradesTmpDir.name
}
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Compiling database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.compileUpgrade, params, token, progress));
}
async function runDatabaseUpgrade(qs: qsClient.QueryServerClient, db: DatabaseItem, upgrades: messages.CompiledUpgrades):
Promise<messages.RunUpgradeResult> {
if (db.contents === undefined || db.contents.datasetUri === undefined) {
throw new Error('Can\'t upgrade an invalid database.');
}
const database: messages.Dataset = {
dbDir: db.contents.datasetUri.fsPath,
workingSet: 'default'
};
const params: messages.RunUpgradeParams = {
db: database,
timeoutSecs: qs.config.timeoutSecs,
toRun: upgrades
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Running database upgrades",
cancellable: true,
}, (progress, token) => qs.sendRequest(messages.runUpgrade, params, token, progress));
}
export async function clearCacheInDatabase(qs: qsClient.QueryServerClient, dbItem: DatabaseItem):
Promise<messages.ClearCacheResult> {
if (dbItem.contents === undefined) {
throw new Error('Can\'t clear the cache in an invalid database.');
}
const db: messages.Dataset = {
dbDir: dbItem.contents.datasetUri.fsPath,
workingSet: 'default',
};
const params: messages.ClearCacheParams = {
dryRun: false,
db,
};
return helpers.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Clearing Cache",
cancellable: false,
}, (progress, token) =>
qs.sendRequest(messages.clearCache, params, token, progress)
);
}
/**
*
* @param filePath This needs to be equivalent to java Path.toRealPath(NO_FOLLOW_LINKS)
*
*/
async function convertToQlPath(filePath: string): Promise<string> {
if (process.platform === "win32") {
if (path.parse(filePath).root === filePath) {
// Java assumes uppercase drive letters are canonical.
return filePath.toUpperCase();
} else {
const dir = await convertToQlPath(path.dirname(filePath));
const fileName = path.basename(filePath);
const fileNames = await promisify<string, string[]>(fs.readdir)(dir);
for (const name of fileNames) {
if (fileName.localeCompare(name, undefined, { sensitivity: 'accent' }) === 0) {
return path.join(dir, name);
}
}
}
throw new Error("Can't convert path to form suitable for QL:" + filePath);
} else {
return filePath;
}
}
/** Gets the selected position within the given editor. */
async function getSelectedPosition(editor: vscode.TextEditor): Promise<messages.Position> {
const pos = editor.selection.start;
const posEnd = editor.selection.end;
// Convert from 0-based to 1-based line and column numbers.
return {
fileName: await convertToQlPath(editor.document.fileName),
line: pos.line + 1, column: pos.character + 1,
endLine: posEnd.line + 1, endColumn: posEnd.character + 1
};
}
/**
* Compare the dbscheme implied by the query `query` and that of the current database.
* If they are compatible, do nothing.
* If they are incompatible but the database can be upgraded, suggest that upgrade.
* If they are incompatible and the database cannot be upgraded, throw an error.
*/
async function checkDbschemeCompatibility(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
query: QueryInfo
): Promise<void> {
const searchPath = helpers.getOnDiskWorkspaceFolders();
if (query.dbItem.contents !== undefined && query.dbItem.contents.dbSchemeUri !== undefined) {
const info = await cliServer.resolveUpgrades(query.dbItem.contents.dbSchemeUri.fsPath, searchPath);
async function hash(filename: string): Promise<string> {
return crypto.createHash('sha256').update(await fs.readFile(filename)).digest('hex');
}
// At this point, we have learned about three dbschemes:
// query.program.dbschemePath is the dbscheme of the actual
// database we're querying.
const dbschemeOfDb = await hash(query.program.dbschemePath);
// query.queryDbScheme is the dbscheme of the query we're
// running, including the library we've resolved it to use.
const dbschemeOfLib = await hash(query.queryDbscheme);
// info.finalDbscheme is which database we're able to upgrade to
const upgradableTo = await hash(info.finalDbscheme);
if (upgradableTo != dbschemeOfLib) {
logger.log(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but database has scheme ${query.program.dbschemePath}, and no upgrade path found`);
throw new Error(`Query ${query.program.queryPath} expects database scheme ${query.queryDbscheme}, but the current database has a different scheme, and no database upgrades are available. The current database scheme may be newer than the CodeQL query libraries in your workspace. Please try using a newer version of the query libraries.`);
}
if (upgradableTo == dbschemeOfLib &&
dbschemeOfDb != dbschemeOfLib) {
// Try to upgrade the database
await upgradeDatabase(
qs,
query.dbItem,
vscode.Uri.file(info.finalDbscheme),
searchPath.map(file => vscode.Uri.file(file))
);
}
}
}
/** Prompts the user to save `document` if it has unsaved changes. */
async function promptUserToSaveChanges(document: vscode.TextDocument) {
if (document.isDirty) {
// TODO: add 'always save' button which records preference in configuration
if (await helpers.showBinaryChoiceDialog('Query file has unsaved changes. Save now?')) {
await document.save();
}
}
}
type SelectedQuery = {
queryPath: string,
quickEvalPosition?: messages.Position
};
/**
* Determines which QL file to run during an invocation of `Run Query` or `Quick Evaluation`, as follows:
* - If the command was called by clicking on a file, then use that file.
* - Otherwise, use the file open in the current editor.
* - In either case, prompt the user to save the file if it is open with unsaved changes.
* - For `Quick Evaluation`, ensure the selected file is also the one open in the editor,
* and use the selected region.
* @param selectedResourceUri The selected resource when the command was run.
* @param quickEval Whether the command being run is `Quick Evaluation`.
*/
async function determineSelectedQuery(selectedResourceUri: vscode.Uri | undefined, quickEval: boolean): Promise<SelectedQuery> {
const editor = vscode.window.activeTextEditor;
// Choose which QL file to use.
let queryUri: vscode.Uri;
if (selectedResourceUri === undefined) {
// No resource was passed to the command handler, so obtain it from the active editor.
// This usually happens when the command is called from the Command Palette.
if (editor === undefined) {
throw new Error('No query was selected. Please select a query and try again.');
} else {
queryUri = editor.document.uri;
}
} else {
// A resource was passed to the command handler, so use it.
queryUri = selectedResourceUri;
}
if (queryUri.scheme !== 'file') {
throw new Error('Can only run queries that are on disk.');
}
const queryPath = queryUri.fsPath;
// Whether we chose the file from the active editor or from a context menu,
// if the same file is open with unsaved changes in the active editor,
// then prompt the user to save it first.
if (editor !== undefined && editor.document.uri.fsPath === queryPath) {
await promptUserToSaveChanges(editor.document);
}
let quickEvalPosition: messages.Position | undefined = undefined;
if (quickEval) {
if (editor == undefined) {
throw new Error('Can\'t run quick evaluation without an active editor.');
}
if (editor.document.fileName !== queryPath) {
// For Quick Evaluation we expect these to be the same.
// Report an error if we end up in this (hopefully unlikely) situation.
throw new Error('The selected resource for quick evaluation should match the active editor.');
}
quickEvalPosition = await getSelectedPosition(editor);
}
return { queryPath, quickEvalPosition };
}
export async function compileAndRunQueryAgainstDatabase(
cliServer: cli.CodeQLCliServer,
qs: qsClient.QueryServerClient,
db: DatabaseItem,
quickEval: boolean,
selectedQueryUri: vscode.Uri | undefined
): Promise<EvaluationInfo> {
if (!db.contents || !db.contents.dbSchemeUri) {
throw new Error(`Database ${db.databaseUri} does not have a CodeQL database scheme.`);
}
// Determine which query to run, based on the selection and the active editor.
const { queryPath, quickEvalPosition } = await determineSelectedQuery(selectedQueryUri, quickEval);
// Get the workspace folder paths.
const diskWorkspaceFolders = helpers.getOnDiskWorkspaceFolders();
// Figure out the library path for the query.
const packConfig = await cliServer.resolveLibraryPath(diskWorkspaceFolders, queryPath);
// Check whether the query has an entirely different schema from the
// database. (Queries that merely need the database to be upgraded
// won't trigger this check)
// This test will produce confusing results if we ever change the name of the database schema files.
const querySchemaName = path.basename(packConfig.dbscheme);
const dbSchemaName = path.basename(db.contents.dbSchemeUri.fsPath);
if (querySchemaName != dbSchemaName) {
logger.log(`Query schema was ${querySchemaName}, but database schema was ${dbSchemaName}.`);
throw new Error(`The query ${path.basename(queryPath)} cannot be run against the selected database: their target languages are different. Please select a different database and try again.`);
}
const qlProgram: messages.QlProgram = {
// The project of the current document determines which library path
// we use. The `libraryPath` field in this server message is relative
// to the workspace root, not to the project root.
libraryPath: packConfig.libraryPath,
// Since we are compiling and running a query against a database,
// we use the database's DB scheme here instead of the DB scheme
// from the current document's project.
dbschemePath: db.contents.dbSchemeUri.fsPath,
queryPath: queryPath
};
// Read the query metadata if possible, to use in the UI.
let metadata: cli.QueryMetadata | undefined;
try {
metadata = await cliServer.resolveMetadata(qlProgram.queryPath);
} catch (e) {
// Ignore errors and provide no metadata.
logger.log(`Couldn't resolve metadata for ${qlProgram.queryPath}: ${e}`);
}
const query = new QueryInfo(qlProgram, db, packConfig.dbscheme, quickEvalPosition, metadata);
await checkDbschemeCompatibility(cliServer, qs, query);
const errors = await query.compile(qs);
if (errors.length == 0) {
const result = await query.run(qs);
return {
query,
result,
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
}
};
} else {
// Error dialogs are limited in size and scrollability,
// so we include a general description of the problem,
// and direct the user to the output window for the detailed compilation messages.
// However we don't show quick eval errors there so we need to display them anyway.
qs.logger.log(`Failed to compile query ${query.program.queryPath} against database scheme ${query.program.dbschemePath}:`);
let formattedMessages: string[] = [];
for (const error of errors) {
const message = error.message || "[no error message available]";
const formatted = `ERROR: ${message} (${error.position.fileName}:${error.position.line}:${error.position.column}:${error.position.endLine}:${error.position.endColumn})`;
formattedMessages.push(formatted);
qs.logger.log(formatted);
}
if (quickEval && formattedMessages.length <= 3) {
helpers.showAndLogErrorMessage("Quick evaluation compilation failed: \n" + formattedMessages.join("\n"));
} else {
helpers.showAndLogErrorMessage((quickEval ? "Quick evaluation" : "Query") +
" compilation failed. Please make sure there are no errors in the query, the database is up to date," +
" and the query and database use the same target language. For more details on the error, go to View > Output," +
" and choose CodeQL Query Server from the dropdown.");
}
return {
query,
result: {
evaluationTime: 0,
resultType: messages.QueryResultType.OTHER_ERROR,
queryId: -1,
runId: -1,
message: "Query had compilation errors"
},
database: {
name: db.name,
databaseUri: db.databaseUri.toString(true)
}
};
}
}

View File

@@ -0,0 +1,182 @@
import * as vscode from 'vscode';
import { ExtensionContext, window as Window } from 'vscode';
import { EvaluationInfo } from './queries';
import * as helpers from './helpers';
import * as messages from './messages';
/**
* query-history.ts
* ------------
* Managing state of previous queries that we've executed.
*
* The source of truth of the current state resides inside the
* `TreeDataProvider` subclass below.
*/
/**
* One item in the user-displayed list of queries that have been run.
*/
export class QueryHistoryItem {
queryName: string;
time: string;
databaseName: string;
info: EvaluationInfo;
constructor(info: EvaluationInfo) {
this.queryName = helpers.getQueryName(info);
this.databaseName = info.database.name;
this.info = info;
this.time = new Date().toLocaleString();
}
get statusString(): string {
switch (this.info.result.resultType) {
case messages.QueryResultType.CANCELLATION:
return `cancelled after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OOM:
return `out of memory`;
case messages.QueryResultType.SUCCESS:
return `finished in ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.TIMEOUT:
return `timed out after ${this.info.result.evaluationTime / 1000} seconds`;
case messages.QueryResultType.OTHER_ERROR:
default:
return `failed`;
}
}
toString(): string {
const { databaseName, queryName, time } = this;
return `[${time}] ${queryName} on ${databaseName} - ${this.statusString}`;
}
}
/**
* Tree data provider for the query history view.
*/
class HistoryTreeDataProvider implements vscode.TreeDataProvider<QueryHistoryItem> {
/**
* XXX: This idiom for how to get a `.fire()`-able event emitter was
* cargo culted from another vscode extension. It seems rather
* involved and I hope there's something better that can be done
* instead.
*/
private _onDidChangeTreeData: vscode.EventEmitter<QueryHistoryItem | undefined> = new vscode.EventEmitter<QueryHistoryItem | undefined>();
readonly onDidChangeTreeData: vscode.Event<QueryHistoryItem | undefined> = this._onDidChangeTreeData.event;
private ctx: ExtensionContext;
private history: QueryHistoryItem[] = [];
/**
* When not undefined, must be reference-equal to an item in `this.databases`.
*/
private current: QueryHistoryItem | undefined;
constructor(ctx: ExtensionContext) {
this.ctx = ctx;
this.history = [];
}
getTreeItem(element: QueryHistoryItem): vscode.TreeItem {
const it = new vscode.TreeItem(element.toString());
it.command = {
title: 'Query History Item',
command: 'codeQLQueryHistory.itemClicked',
arguments: [element],
};
return it;
}
getChildren(element?: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem[]> {
if (element == undefined) {
return this.history;
}
else {
return [];
}
}
getParent(element: QueryHistoryItem): vscode.ProviderResult<QueryHistoryItem> {
return null;
}
getCurrent(): QueryHistoryItem | undefined {
return this.current;
}
push(item: QueryHistoryItem): void {
this.current = item;
this.history.push(item);
this._onDidChangeTreeData.fire();
}
setCurrentItem(item: QueryHistoryItem) {
this.current = item;
}
}
/**
* Number of milliseconds two clicks have to arrive apart to be
* considered a double-click.
*/
const DOUBLE_CLICK_TIME = 500;
export class QueryHistoryManager {
treeDataProvider: HistoryTreeDataProvider;
ctx: ExtensionContext;
treeView: vscode.TreeView<QueryHistoryItem>;
selectedCallback: ((item: QueryHistoryItem) => void) | undefined;
lastItemClick: { time: Date, item: QueryHistoryItem } | undefined;
async handleOpenQuery(queryHistoryItem: QueryHistoryItem) {
const textDocument = await vscode.workspace.openTextDocument(vscode.Uri.file(queryHistoryItem.info.query.program.queryPath));
await vscode.window.showTextDocument(textDocument, vscode.ViewColumn.One);
}
async handleItemClicked(queryHistoryItem: QueryHistoryItem) {
this.treeDataProvider.setCurrentItem(queryHistoryItem);
const now = new Date();
const prevItemClick = this.lastItemClick;
this.lastItemClick = { time: now, item: queryHistoryItem };
if (prevItemClick !== undefined
&& (now.valueOf() - prevItemClick.time.valueOf()) < DOUBLE_CLICK_TIME
&& queryHistoryItem == prevItemClick.item) {
// show original query file on double click
await this.handleOpenQuery(queryHistoryItem);
}
else {
// show results on single click
if (this.selectedCallback !== undefined) {
const sc = this.selectedCallback;
await sc(queryHistoryItem);
}
}
}
constructor(ctx: ExtensionContext, selectedCallback?: (item: QueryHistoryItem) => Promise<void>) {
this.ctx = ctx;
this.selectedCallback = selectedCallback;
const treeDataProvider = this.treeDataProvider = new HistoryTreeDataProvider(ctx);
this.treeView = Window.createTreeView('codeQLQueryHistory', { treeDataProvider });
this.treeView.onDidChangeSelection(async ev => {
if (ev.selection.length == 0) {
const current = this.treeDataProvider.getCurrent();
if (current != undefined)
this.treeView.reveal(current); // don't allow selection to become empty
}
});
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.openQuery', this.handleOpenQuery));
ctx.subscriptions.push(vscode.commands.registerCommand('codeQLQueryHistory.itemClicked', async (item) => {
return this.handleItemClicked(item);
}));
}
push(item: QueryHistoryItem) {
this.treeDataProvider.push(item);
this.treeView.reveal(item, { select: true });
}
}

View File

@@ -0,0 +1,163 @@
import * as cp from 'child_process';
import { DisposableObject } from 'semmle-vscode-utils';
import { Disposable } from 'vscode';
import { CancellationToken, createMessageConnection, MessageConnection, RequestType } from 'vscode-jsonrpc';
import * as cli from './cli';
import { QueryServerConfig } from './config';
import { Logger, ProgressReporter } from './logging';
import { completeQuery, EvaluationResult, progress, ProgressMessage, WithProgressId } from './messages';
type ServerOpts = {
logger: Logger
}
/** A running query server process and its associated message connection. */
class ServerProcess implements Disposable {
child: cp.ChildProcess;
connection: MessageConnection;
logger: Logger;
constructor(child: cp.ChildProcess, connection: MessageConnection, logger: Logger) {
this.child = child;
this.connection = connection;
this.logger = logger;
}
dispose() {
this.logger.log('Stopping query server...');
this.connection.dispose();
this.child.stdin!.end();
this.child.stderr!.destroy();
// TODO kill the process if it doesn't terminate after a certain time limit.
// On Windows, we usually have to terminate the process before closing its stdout.
this.child.stdout!.destroy();
this.logger.log('Stopped query server.');
}
}
type WithProgressReporting = (task: (progress: ProgressReporter, token: CancellationToken) => Thenable<void>) => Thenable<void>;
/**
* Client that manages a query server process.
* The server process is started upon initialization and tracked during its lifetime.
* The server process is disposed when the client is disposed, or if the client asks
* to restart it (which disposes the existing process and starts a new one).
*/
export class QueryServerClient extends DisposableObject {
serverProcess?: ServerProcess;
evaluationResultCallbacks: { [key: number]: (res: EvaluationResult) => void };
progressCallbacks: { [key: number]: ((res: ProgressMessage) => void) | undefined };
nextCallback: number;
nextProgress: number;
withProgressReporting: WithProgressReporting;
constructor(readonly config: QueryServerConfig, readonly cliServer: cli.CodeQLCliServer, readonly opts: ServerOpts, withProgressReporting: WithProgressReporting) {
super();
// When the query server configuration changes, restart the query server.
if (config.onDidChangeQueryServerConfiguration !== undefined) {
this.push(config.onDidChangeQueryServerConfiguration(async () => await this.restartQueryServer(), this));
}
this.withProgressReporting = withProgressReporting;
this.nextCallback = 0;
this.nextProgress = 0;
this.progressCallbacks = {};
this.evaluationResultCallbacks = {};
}
get logger() { return this.opts.logger; }
/** Stops the query server by disposing of the current server process. */
private stopQueryServer() {
if (this.serverProcess !== undefined) {
this.disposeAndStopTracking(this.serverProcess);
} else {
this.logger.log('No server process to be stopped.')
}
}
/** Restarts the query server by disposing of the current server process and then starting a new one. */
private async restartQueryServer() {
this.logger.log('Restarting query server due to configuration changes...');
this.stopQueryServer();
await this.startQueryServer();
}
/** Starts a new query server process, sending progress messages to the status bar. */
async startQueryServer() {
// Use an arrow function to preserve the value of `this`.
return this.withProgressReporting((progress, _) => this.startQueryServerImpl(progress));
}
/** Starts a new query server process, sending progress messages to the given reporter. */
private async startQueryServerImpl(progressReporter: ProgressReporter) {
const ramArgs = await this.cliServer.resolveRam(this.config.queryMemoryMb, progressReporter);
const args = ['--threads', this.config.numThreads.toString()].concat(ramArgs);
if (this.config.debug) {
args.push('--debug', '--tuple-counting');
}
const child = cli.spawnServer(
this.config.codeQlPath,
'CodeQL query server',
['execute', 'query-server'],
args,
this.logger,
data => this.logger.logWithoutTrailingNewline(data.toString()),
undefined, // no listener for stdout
progressReporter
);
progressReporter.report({ message: 'Connecting to CodeQL query server' });
const connection = createMessageConnection(child.stdout, child.stdin);
connection.onRequest(completeQuery, res => {
if (!(res.runId in this.evaluationResultCallbacks)) {
this.logger.log(`No callback associated with run id ${res.runId}, continuing without executing any callback`);
}
else {
this.evaluationResultCallbacks[res.runId](res);
}
return {};
})
connection.onNotification(progress, res => {
let callback = this.progressCallbacks[res.id];
if (callback) {
callback(res);
}
})
this.serverProcess = new ServerProcess(child, connection, this.opts.logger);
// Ensure the server process is disposed together with this client.
this.track(this.serverProcess);
connection.listen();
progressReporter.report({ message: 'Connected to CodeQL query server' });
this.nextCallback = 0;
this.nextProgress = 0;
this.progressCallbacks = {};
this.evaluationResultCallbacks = {};
}
registerCallback(callback: (res: EvaluationResult) => void): number {
const id = this.nextCallback++;
this.evaluationResultCallbacks[id] = callback;
return id;
}
unRegisterCallback(id: number) {
delete this.evaluationResultCallbacks[id];
}
get serverProcessPid(): number {
return this.serverProcess!.child.pid;
}
async sendRequest<P, R, E, RO>(type: RequestType<WithProgressId<P>, R, E, RO>, parameter: P, token?: CancellationToken, progress?: (res: ProgressMessage) => void): Promise<R> {
let id = this.nextProgress++;
this.progressCallbacks[id] = progress;
try {
if (this.serverProcess === undefined) {
throw new Error('No query server process found.');
}
return await this.serverProcess.connection.sendRequest(type, { body: parameter, progressId: id }, token);
} finally {
delete this.progressCallbacks[id];
}
}
}

View File

@@ -0,0 +1,398 @@
Content from https://github.com/microsoft/vscode-icons used
under the Creative Commons License as follows:
Attribution 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More_considerations
for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution 4.0 International Public License ("Public License"). To the
extent this Public License may be interpreted as a contract, You are
granted the Licensed Rights in consideration of Your acceptance of
these terms and conditions, and the Licensor grants You such rights in
consideration of benefits the Licensor receives from making the
Licensed Material available under these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
d. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
g. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
i. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient of the Licensed
Material.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the Adapted
Material from complying with this Public License.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the “Licensor.” The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material. For
the avoidance of doubt, this paragraph does not form part of the
public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -0,0 +1,349 @@
import cx from 'classnames';
import * as path from 'path';
import * as React from 'react';
import * as Sarif from 'sarif';
import { LocationStyle, ResolvableLocationValue } from 'semmle-bqrs';
import * as octicons from './octicons';
import { className, renderLocation, ResultTableProps, selectedClassName, zebraStripe } from './result-table-utils';
import { PathTableResultSet } from './results';
export type PathTableProps = ResultTableProps & { resultSet: PathTableResultSet };
export interface PathTableState {
expanded: { [k: string]: boolean };
}
interface SarifLink {
dest: number
text: string
}
type ParsedSarifLocation =
| ResolvableLocationValue
// Resolvable locations have a `file` field, but it will sometimes include
// a source location prefix, which contains build-specific information the user
// doesn't really need to see. We ensure that `userVisibleFile` will not contain
// that, and is appropriate for display in the UI.
& { userVisibleFile: string }
| { t: 'NoLocation', hint: string };
type SarifMessageComponent = string | SarifLink
/**
* Unescape "[", "]" and "\\" like in sarif plain text messages
*/
function unescapeSarifText(message: string): string {
return message.replace(/\\\[/g, "[").replace(/\\\]/g, "]").replace(/\\\\/, "\\");
}
function parseSarifPlainTextMessage(message: string): SarifMessageComponent[] {
let results: SarifMessageComponent[] = [];
// We want something like "[linkText](4)", except that "[" and "]" may be escaped. The lookbehind asserts
// that the initial [ is not escaped. Then we parse a link text with "[" and "]" escaped. Then we parse the numerical target.
// Technically we could have any uri in the target but we don't output that yet.
// The possibility of escaping outside the link is not mentioned in the sarif spec but we always output sartif this way.
const linkRegex = /(?<=(?<!\\)(\\\\)*)\[(?<linkText>([^\\\]\[]|\\\\|\\\]|\\\[)*)\]\((?<linkTarget>[0-9]+)\)/g;
let result: RegExpExecArray | null;
let curIndex = 0;
while ((result = linkRegex.exec(message)) !== null) {
results.push(unescapeSarifText(message.substring(curIndex, result.index)));
const linkText = result.groups!["linkText"];
const linkTarget = +result.groups!["linkTarget"];
results.push({ dest: linkTarget, text: unescapeSarifText(linkText) });
curIndex = result.index + result[0].length;
}
results.push(unescapeSarifText(message.substring(curIndex, message.length)));
return results;
}
/**
* Computes a path normalized to reflect conventional normalization
* of windows paths into zip archive paths.
* @param sourceLocationPrefix The source location prefix of a database. May be
* unix style `/foo/bar/baz` or windows-style `C:\foo\bar\baz`.
* @param sarifRelativeUri A uri relative to sourceLocationPrefix.
* @returns A string that is valid for the `.file` field of a `FivePartLocation`:
* directory separators are normalized, but drive letters `C:` may appear.
*/
export function getPathRelativeToSourceLocationPrefix(sourceLocationPrefix: string, sarifRelativeUui: string) {
const normalizedSourceLocationPrefix = sourceLocationPrefix.replace(/\\/g, '/');
return path.join(normalizedSourceLocationPrefix, decodeURIComponent(sarifRelativeUui));
}
export class PathTable extends React.Component<PathTableProps, PathTableState> {
constructor(props: PathTableProps) {
super(props);
this.state = { expanded: {} };
}
/**
* Given a list of `indices`, toggle the first, and if we 'open' the
* first item, open all the rest as well. This mimics vscode's file
* explorer tree view behavior.
*/
toggle(e: React.MouseEvent, indices: number[]) {
this.setState(previousState => {
if (previousState.expanded[indices[0]]) {
return { expanded: { ...previousState.expanded, [indices[0]]: false } };
}
else {
const expanded = { ...previousState.expanded };
for (const index of indices) {
expanded[index] = true;
}
return { expanded };
}
});
e.stopPropagation();
e.preventDefault();
}
render(): JSX.Element {
const { selected, databaseUri, resultSet } = this.props;
const tableClassName = cx(className, {
[selectedClassName]: selected
});
const rows: JSX.Element[] = [];
const { numTruncatedResults, sourceLocationPrefix } = resultSet;
function renderRelatedLocations(msg: string, relatedLocations: Sarif.Location[]): JSX.Element[] {
const relatedLocationsById: { [k: string]: Sarif.Location } = {};
for (let loc of relatedLocations) {
relatedLocationsById[loc.id!] = loc;
}
const result: JSX.Element[] = [];
// match things like `[link-text](related-location-id)`
const parts = parseSarifPlainTextMessage(msg);
for (const part of parts) {
if (typeof part === "string") {
result.push(<span>{part} </span>);
} else {
const renderedLocation = renderSarifLocationWithText(part.text, relatedLocationsById[part.dest]);
result.push(<span>{renderedLocation} </span>);
}
} return result;
}
function renderNonLocation(msg: string | undefined, locationHint: string): JSX.Element | undefined {
if (msg == undefined)
return undefined;
return <span title={locationHint}>{msg}</span>;
}
function parseSarifLocation(loc: Sarif.Location): ParsedSarifLocation {
const physicalLocation = loc.physicalLocation;
if (physicalLocation === undefined)
return { t: 'NoLocation', hint: 'no physical location' };
if (physicalLocation.artifactLocation === undefined)
return { t: 'NoLocation', hint: 'no artifact location' };
if (physicalLocation.artifactLocation.uri === undefined)
return { t: 'NoLocation', hint: 'artifact location has no uri' };
// This is not necessarily really an absolute uri; it could either be a
// file uri or a relative uri.
const uri = physicalLocation.artifactLocation.uri;
const fileUriRegex = /^file:/;
const effectiveLocation = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
getPathRelativeToSourceLocationPrefix(sourceLocationPrefix, uri);
const userVisibleFile = uri.match(fileUriRegex) ?
decodeURIComponent(uri.replace(fileUriRegex, '')) :
uri;
if (physicalLocation.region === undefined) {
// If the region property is absent, the physicalLocation object refers to the entire file.
// Source: https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Toc16012638.
// TODO: Do we get here if we provide a non-filesystem URL?
return {
t: LocationStyle.WholeFile,
file: effectiveLocation,
userVisibleFile,
};
} else {
const region = physicalLocation.region;
// We assume that the SARIF we're given always has startLine
// This is not mandated by the SARIF spec, but should be true of
// SARIF output by our own tools.
const lineStart = region.startLine!;
// These defaults are from SARIF 2.1.0 spec, section 3.30.2, "Text Regions"
// https://docs.oasis-open.org/sarif/sarif/v2.1.0/cs01/sarif-v2.1.0-cs01.html#_Ref493492556
const lineEnd = region.endLine === undefined ? lineStart : region.endLine;
const colStart = region.startColumn === undefined ? 1 : region.startColumn;
// We also assume that our tools will always supply `endColumn` field, which is
// fortunate, since the SARIF spec says that it defaults to the end of the line, whose
// length we don't know at this point in the code.
//
// It is off by one with respect to the way vscode counts columns in selections.
const colEnd = region.endColumn! - 1;
return {
t: LocationStyle.FivePart,
file: effectiveLocation,
userVisibleFile,
lineStart,
colStart,
lineEnd,
colEnd,
};
}
}
function renderSarifLocationWithText(text: string | undefined, loc: Sarif.Location): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc);
switch (parsedLoc.t) {
case 'NoLocation':
return renderNonLocation(text, parsedLoc.hint);
case LocationStyle.FivePart:
case LocationStyle.WholeFile:
return renderLocation(parsedLoc, text, databaseUri);
}
return undefined;
}
/**
* Render sarif location as a link with the text being simply a
* human-readable form of the location itself.
*/
function renderSarifLocation(loc: Sarif.Location): JSX.Element | undefined {
const parsedLoc = parseSarifLocation(loc);
let shortLocation, longLocation: string;
switch (parsedLoc.t) {
case 'NoLocation':
return renderNonLocation("[no location]", parsedLoc.hint);
case LocationStyle.WholeFile:
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
case LocationStyle.FivePart:
shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.lineStart}:${parsedLoc.colStart}`;
longLocation = `${parsedLoc.userVisibleFile}`;
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation);
}
}
const toggler: (indices: number[]) => (e: React.MouseEvent) => void = (indices) => {
return (e) => this.toggle(e, indices);
};
const noResults = <span>No Results</span>; // TODO: Maybe make this look nicer
let expansionIndex = 0;
if (resultSet.sarif.runs.length === 0) return noResults;
if (resultSet.sarif.runs[0].results === undefined) return noResults;
resultSet.sarif.runs[0].results.forEach((result, resultIndex) => {
const text = result.message.text || '[no text]';
const msg: JSX.Element[] =
result.relatedLocations === undefined ?
[<span>{text}</span>] :
renderRelatedLocations(text, result.relatedLocations);
const currentResultExpanded = this.state.expanded[expansionIndex];
const indicator = currentResultExpanded ? octicons.chevronDown : octicons.chevronRight;
const location = result.locations !== undefined && result.locations.length > 0 &&
renderSarifLocation(result.locations[0]);
const locationCells = <td className="vscode-codeql__location-cell">{location}</td>;
if (result.codeFlows === undefined) {
rows.push(
<tr {...zebraStripe(resultIndex)}>
<td className="vscode-codeql__icon-cell">{octicons.info}</td>
<td colSpan={3}>
{msg}
</td>
{locationCells}
</tr>
);
}
else {
const paths: Sarif.ThreadFlow[] = [];
for (const codeFlow of result.codeFlows) {
for (const threadFlow of codeFlow.threadFlows) {
paths.push(threadFlow);
}
}
const indices = paths.length == 1 ?
[expansionIndex, expansionIndex + 1] : /* if there's exactly one path, auto-expand
* the path when expanding the result */
[expansionIndex];
rows.push(
<tr {...zebraStripe(resultIndex)}>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
{indicator}
</td>
<td className="vscode-codeql__icon-cell">
{octicons.listUnordered}
</td>
<td colSpan={2}>
{msg}
</td>
{locationCells}
</tr >
);
expansionIndex++;
paths.forEach(path => {
const currentPathExpanded = this.state.expanded[expansionIndex];
if (currentResultExpanded) {
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
rows.push(
<tr {...zebraStripe(resultIndex)}>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td>
<td className="vscode-codeql__text-center" colSpan={3}>
Path
</td>
</tr>
);
}
expansionIndex++;
if (currentResultExpanded && currentPathExpanded) {
let pathIndex = 1;
for (const step of path.locations) {
const msg = step.location !== undefined && step.location.message !== undefined ?
renderSarifLocationWithText(step.location.message.text, step.location) :
'[no location]';
const additionalMsg = step.location !== undefined ?
renderSarifLocation(step.location) :
'';
const stepIndex = resultIndex + pathIndex;
rows.push(
<tr>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
<td {...zebraStripe(stepIndex, 'vscode-codeql__path-index-cell')}>{pathIndex}</td>
<td {...zebraStripe(stepIndex)}>{msg} </td>
<td {...zebraStripe(stepIndex, 'vscode-codeql__location-cell')}>{additionalMsg}</td>
</tr>);
pathIndex++;
}
}
});
}
});
if (numTruncatedResults > 0) {
rows.push(<tr><td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
Too many results to show at once. {numTruncatedResults} result(s) omitted.
</td></tr>);
}
return <table className={tableClassName}>
<tbody>{rows}</tbody>
</table>;
}
}

View File

@@ -0,0 +1,22 @@
import * as React from "react";
/**
* These icons come from https://github.com/microsoft/vscode-icons
* and are used under the Creative Commons License, see `LICENSE`
* file in this directory.
*/
export const chevronDown = <svg className="octicon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" >
<path fillRule="evenodd" clipRule="evenodd" d="M7.976 10.072l4.357-4.357.62.618L8.284 11h-.618L3 6.333l.62-.618 4.356 4.357z" />
</svg>;
export const chevronRight = <svg className="octicon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" >
<path d="M5.7 13.7L5 13l4.6-4.6L5 3.7l.7-.7 5 5v.7l-5 5z" />
</svg>;
export const listUnordered = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" >
<path fillRule="evenodd" clipRule="evenodd" d="M2 3H1v1h1V3zm0 3H1v1h1V6zM1 9h1v1H1V9zm1 3H1v1h1v-1zm2-9h11v1H4V3zm11 3H4v1h11V6zM4 9h11v1H4V9zm11 3H4v1h11v-1z" />
</svg>;
export const info = <svg className="octicon octicon-light" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" >
<path fillRule="evenodd" clipRule="evenodd" d="M8.568 1.03a6.8 6.8 0 0 1 4.192 2.02 7.06 7.06 0 0 1 .46 9.39 6.85 6.85 0 0 1-8.58 1.74 7 7 0 0 1-3.12-3.5 7.12 7.12 0 0 1-.23-4.71 7 7 0 0 1 2.77-3.79 6.8 6.8 0 0 1 4.508-1.15zm.472 12.85a5.89 5.89 0 0 0 3.41-2.07 6.07 6.07 0 0 0-.4-8.06 5.82 5.82 0 0 0-7.43-.74 6.06 6.06 0 0 0 .5 10.29 5.81 5.81 0 0 0 3.92.58zM8.51 7h-1v4h1V7zm0-2h-1v1h1V5z" />
</svg>;

View File

@@ -0,0 +1,118 @@
import cx from 'classnames';
import * as React from "react";
import { className, renderLocation, ResultTableProps, selectedClassName, zebraStripe } from "./result-table-utils";
import { RawTableResultSet, ResultValue, vscode } from "./results";
import { assertNever } from "../helpers-pure";
import { SortDirection, SortState, RAW_RESULTS_LIMIT } from "../interface-types";
export type RawTableProps = ResultTableProps & {
resultSet: RawTableResultSet,
sortState?: SortState;
};
export class RawTable extends React.Component<RawTableProps, {}> {
constructor(props: RawTableProps) {
super(props);
}
render(): React.ReactNode {
const { resultSet, selected, databaseUri } = this.props;
const tableClassName = cx(className, {
[selectedClassName]: selected
});
let dataRows = this.props.resultSet.rows;
let numTruncatedResults = 0;
if (dataRows.length > RAW_RESULTS_LIMIT) {
numTruncatedResults = dataRows.length - RAW_RESULTS_LIMIT;
dataRows = dataRows.slice(0, RAW_RESULTS_LIMIT);
}
const tableRows = dataRows.map((row, rowIndex) =>
<tr key={rowIndex} {...zebraStripe(rowIndex)}>
{
[
<td key={-1}>{rowIndex + 1}</td>,
...row.map((value, columnIndex) =>
<td key={columnIndex}>
{
renderTupleValue(value, databaseUri)
}
</td>)
]
}
</tr>
);
if (numTruncatedResults > 0) {
const colSpan = dataRows[0].length + 1; // one row for each data column, plus index column
tableRows.push(<tr><td key={'message'} colSpan={colSpan} style={{ textAlign: 'center', fontStyle: 'italic' }}>
Too many results to show at once. {numTruncatedResults} result(s) omitted.
</td></tr>);
}
return <table className={tableClassName}>
<thead>
<tr>
{
[
<th key={-1}><b>#</b></th>,
...resultSet.schema.columns.map((col, index) => {
const displayName = col.name || `[${index}]`;
const sortDirection = this.props.sortState && index === this.props.sortState.columnIndex ? this.props.sortState.direction : undefined;
return <th className={"sort-" + (sortDirection !== undefined ? SortDirection[sortDirection] : "none")} key={index} onClick={() => this.toggleSortStateForColumn(index)}><b>{displayName}</b></th>;
})
]
}
</tr>
</thead>
<tbody>
{tableRows}
</tbody>
</table>;
}
private toggleSortStateForColumn(index: number) {
const sortState = this.props.sortState;
const prevDirection = sortState && sortState.columnIndex === index ? sortState.direction : undefined;
const nextDirection = nextSortDirection(prevDirection);
const nextSortState = nextDirection === undefined ? undefined : {
columnIndex: index,
direction: nextDirection
};
vscode.postMessage({
t: 'changeSort',
resultSetName: this.props.resultSet.schema.name,
sortState: nextSortState
});
}
}
/**
* Render one column of a tuple.
*/
function renderTupleValue(v: ResultValue, databaseUri: string): JSX.Element {
if (typeof v === 'string') {
return <span>{v}</span>
}
else if ('uri' in v) {
return <a href={v.uri}>{v.uri}</a>;
}
else {
return renderLocation(v.location, v.label, databaseUri);
}
}
function nextSortDirection(direction: SortDirection | undefined): SortDirection {
switch (direction) {
case SortDirection.asc:
return SortDirection.desc;
case SortDirection.desc:
case undefined:
return SortDirection.asc;
default:
return assertNever(direction);
}
}

View File

@@ -0,0 +1,70 @@
import * as React from 'react';
import { LocationValue, ResolvableLocationValue, tryGetResolvableLocation } from 'semmle-bqrs';
import { SortState } from '../interface-types';
import { ResultSet, vscode } from './results';
export interface ResultTableProps {
selected: boolean;
resultSet: ResultSet;
databaseUri: string;
resultsPath: string | undefined;
sortState?: SortState;
}
export const className = 'vscode-codeql__result-table';
export const tableSelectionHeaderClassName = 'vscode-codeql__table-selection-header';
export const toggleDiagnosticsClassName = `${className}-toggle-diagnostics`;
export const selectedClassName = `${className}--selected`;
export const toggleDiagnosticsSelectedClassName = `${toggleDiagnosticsClassName}--selected`;
export const evenRowClassName = 'vscode-codeql__result-table-row--even';
export const oddRowClassName = 'vscode-codeql__result-table-row--odd';
export const pathRowClassName = 'vscode-codeql__result-table-row--path';
export function jumpToLocationHandler(
loc: ResolvableLocationValue,
databaseUri: string
): (e: React.MouseEvent) => void {
return (e) => {
vscode.postMessage({
t: 'viewSourceFile',
loc,
databaseUri
});
e.preventDefault();
e.stopPropagation();
};
}
/**
* Render a location as a link which when clicked displays the original location.
*/
export function renderLocation(loc: LocationValue | undefined, label: string | undefined,
databaseUri: string, title?: string): JSX.Element {
// If the label was empty, use a placeholder instead, so the link is still clickable.
let displayLabel = label;
if (label === undefined || label === '')
displayLabel = '[empty string]';
else if (label.match(/^\s+$/))
displayLabel = `[whitespace: "${label}"]`;
if (loc !== undefined) {
const resolvableLoc = tryGetResolvableLocation(loc);
if (resolvableLoc !== undefined) {
return <a href="#"
className="vscode-codeql__result-table-location-link"
title={title}
onClick={jumpToLocationHandler(resolvableLoc, databaseUri)}>{displayLabel}</a>;
} else {
return <span title={title}>{displayLabel}</span>;
}
}
return <span />
}
/**
* Returns the attributes for a zebra-striped table row at position `index`.
*/
export function zebraStripe(index: number, ...otherClasses: string[]): { className: string } {
return { className: [(index % 2) ? oddRowClassName : evenRowClassName, otherClasses].join(' ') };
}

View File

@@ -0,0 +1,147 @@
import cx from 'classnames';
import * as React from 'react';
import { DatabaseInfo, Interpretation, SortState } from '../interface-types';
import { PathTable } from './alert-table';
import { RawTable } from './raw-results-table';
import { ResultTableProps, toggleDiagnosticsClassName, toggleDiagnosticsSelectedClassName, tableSelectionHeaderClassName } from './result-table-utils';
import { ResultSet, vscode } from './results';
/**
* Properties for the `ResultTables` component.
*/
export interface ResultTablesProps {
rawResultSets: readonly ResultSet[];
interpretation: Interpretation | undefined;
database: DatabaseInfo;
resultsPath: string | undefined;
kind: string | undefined;
sortStates: Map<string, SortState>;
isLoadingNewResults: boolean;
}
/**
* State for the `ResultTables` component.
*/
interface ResultTablesState {
selectedTable: string; // name of selected result set
}
const ALERTS_TABLE_NAME = 'alerts';
const SELECT_TABLE_NAME = '#select';
const UPDATING_RESULTS_TEXT_CLASS_NAME = "vscode-codeql__result-tables-updating-text";
/**
* Displays multiple `ResultTable` tables, where the table to be displayed is selected by a
* dropdown.
*/
export class ResultTables
extends React.Component<ResultTablesProps, ResultTablesState> {
private getResultSets(): ResultSet[] {
const resultSets: ResultSet[] =
this.props.rawResultSets.map(rs => ({ t: 'RawResultSet', ...rs }));
if (this.props.interpretation != undefined) {
resultSets.push({
t: 'SarifResultSet',
// FIXME: The values of version, columns, tupleCount are
// unused stubs because a SarifResultSet schema isn't used the
// same way as a RawResultSet. Probably should pull `name` field
// out.
schema: { name: ALERTS_TABLE_NAME, version: 0, columns: [], tupleCount: 1 },
name: ALERTS_TABLE_NAME,
...this.props.interpretation,
});
}
return resultSets;
}
constructor(props: ResultTablesProps) {
super(props);
this.state = {
// Get the result set that should be displayed by default
selectedTable: ResultTables.getDefaultResultSet(this.getResultSets())
};
}
private static getDefaultResultSet(resultSets: readonly ResultSet[]): string {
const resultSetNames = resultSets.map(resultSet => resultSet.schema.name)
// Choose first available result set from the array
return [ALERTS_TABLE_NAME, SELECT_TABLE_NAME, resultSets[0].schema.name].filter(resultSetName => resultSetNames.includes(resultSetName))[0];
}
private onChange = (event: React.ChangeEvent<HTMLSelectElement>): void => {
this.setState({ selectedTable: event.target.value });
}
render(): React.ReactNode {
const selectedTable = this.state.selectedTable;
const resultSets = this.getResultSets();
const { database, resultsPath, kind } = this.props;
// Only show the Problems view display checkbox for the alerts table.
const toggleDiagnosticsClass = cx(toggleDiagnosticsClassName, {
[toggleDiagnosticsSelectedClassName]: selectedTable === ALERTS_TABLE_NAME
});
return <div>
<div className={tableSelectionHeaderClassName}>
<select value={selectedTable} onChange={this.onChange}>
{
resultSets.map(resultSet =>
<option key={resultSet.schema.name} value={resultSet.schema.name}>
{resultSet.schema.name}
</option>
)
}
</select>
<div className={toggleDiagnosticsClass}>
<input type="checkbox" id="toggle-diagnostics" name="toggle-diagnostics" onChange={(e) => {
if (resultsPath !== undefined) {
vscode.postMessage({
t: 'toggleDiagnostics',
resultsPath: resultsPath,
databaseUri: database.databaseUri,
visible: e.target.checked,
kind: kind
});
}
}} />
<label htmlFor="toggle-diagnostics">Show results in Problems view</label>
</div>
{
this.props.isLoadingNewResults ?
<span className={UPDATING_RESULTS_TEXT_CLASS_NAME}>Updating results</span>
: null
}
</div>
{
resultSets.map(resultSet =>
<ResultTable key={resultSet.schema.name} resultSet={resultSet}
databaseUri={this.props.database.databaseUri} selected={resultSet.schema.name === selectedTable}
resultsPath={this.props.resultsPath} sortState={this.props.sortStates.get(resultSet.schema.name)} />
)
}
</div>;
}
}
class ResultTable extends React.Component<ResultTableProps, {}> {
constructor(props: ResultTableProps) {
super(props);
}
render(): React.ReactNode {
const { resultSet } = this.props;
switch (resultSet.t) {
case 'RawResultSet': return <RawTable
selected={this.props.selected} resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} sortState={this.props.sortState} />;
case 'SarifResultSet': return <PathTable
selected={this.props.selected} resultSet={resultSet} databaseUri={this.props.databaseUri}
resultsPath={this.props.resultsPath} />;
}
}
}

View File

@@ -0,0 +1,329 @@
import * as React from 'react';
import * as Rdom from 'react-dom';
import * as bqrs from 'semmle-bqrs';
import { ElementBase, LocationValue, PrimitiveColumnValue, PrimitiveTypeKind, ResultSetSchema, tryGetResolvableLocation } from 'semmle-bqrs';
import { assertNever } from '../helpers-pure';
import { DatabaseInfo, FromResultsViewMsg, Interpretation, IntoResultsViewMsg, SortedResultSetInfo, SortState } from '../interface-types';
import { ResultTables } from './result-tables';
/**
* results.tsx
* -----------
*
* Displaying query results.
*/
interface VsCodeApi {
/**
* Post message back to vscode extension.
*/
postMessage(msg: FromResultsViewMsg): void;
}
declare const acquireVsCodeApi: () => VsCodeApi;
export const vscode = acquireVsCodeApi();
export interface ResultElement {
label: string,
location?: LocationValue
}
export interface ResultUri {
uri: string;
}
export type ResultValue = ResultElement | ResultUri | string;
export type ResultRow = ResultValue[];
export type RawTableResultSet = { t: 'RawResultSet' } & RawResultSet;
export type PathTableResultSet = { t: 'SarifResultSet', readonly schema: ResultSetSchema, name: string } & Interpretation;
export type ResultSet =
| RawTableResultSet
| PathTableResultSet;
export interface RawResultSet {
readonly schema: ResultSetSchema;
readonly rows: readonly ResultRow[];
}
async function* getChunkIterator(response: Response): AsyncIterableIterator<Uint8Array> {
if (!response.ok) {
throw new Error(`Failed to load results: (${response.status}) ${response.statusText}`);
}
const reader = response.body!.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) {
return;
}
yield value;
}
}
function translatePrimitiveValue(value: PrimitiveColumnValue, type: PrimitiveTypeKind):
ResultValue {
switch (type) {
case 'i':
case 'f':
case 's':
case 'd':
case 'b':
return value.toString();
case 'u':
return {
uri: value as string
};
}
}
async function parseResultSets(response: Response): Promise<readonly ResultSet[]> {
const chunks = getChunkIterator(response);
const resultSets: ResultSet[] = [];
await bqrs.parse(chunks, (resultSetSchema) => {
const columnTypes = resultSetSchema.columns.map((column) => column.type);
const rows: ResultRow[] = [];
resultSets.push({
t: 'RawResultSet',
schema: resultSetSchema,
rows: rows
});
return (tuple) => {
const row: ResultValue[] = [];
tuple.forEach((value, index) => {
const type = columnTypes[index];
if (type.type === 'e') {
const element: ElementBase = value as ElementBase;
const label = (element.label !== undefined) ? element.label : element.id.toString(); //REVIEW: URLs?
const resolvableLocation = tryGetResolvableLocation(element.location);
if (resolvableLocation !== undefined) {
row.push({
label: label,
location: resolvableLocation
});
}
else {
// No location link.
row.push(label);
}
}
else {
row.push(translatePrimitiveValue(value as PrimitiveColumnValue, type.type));
}
});
rows.push(row);
};
});
return resultSets;
}
interface ResultsInfo {
resultsPath: string;
kind: string | undefined;
database: DatabaseInfo;
interpretation: Interpretation | undefined;
sortedResultsMap: Map<string, SortedResultSetInfo>;
/**
* See {@link SetStateMsg.shouldKeepOldResultsWhileRendering}.
*/
shouldKeepOldResultsWhileRendering: boolean;
}
interface Results {
resultSets: readonly ResultSet[];
sortStates: Map<string, SortState>;
database: DatabaseInfo;
}
interface ResultsState {
// We use `null` instead of `undefined` here because in React, `undefined` is
// used to mean "did not change" when updating the state of a component.
resultsInfo: ResultsInfo | null;
results: Results | null;
errorMessage: string;
}
interface ResultsViewState {
displayedResults: ResultsState;
nextResultsInfo: ResultsInfo | null;
isExpectingResultsUpdate: boolean;
}
/**
* A minimal state container for displaying results.
*/
class App extends React.Component<{}, ResultsViewState> {
constructor(props: any) {
super(props);
this.state = {
displayedResults: {
resultsInfo: null,
results: null,
errorMessage: ''
},
nextResultsInfo: null,
isExpectingResultsUpdate: true
};
}
handleMessage(msg: IntoResultsViewMsg): void {
switch (msg.t) {
case 'setState':
this.updateStateWithNewResultsInfo({
resultsPath: msg.resultsPath,
kind: msg.kind,
sortedResultsMap: new Map(Object.entries(msg.sortedResultsMap)),
database: msg.database,
interpretation: msg.interpretation,
shouldKeepOldResultsWhileRendering: msg.shouldKeepOldResultsWhileRendering
});
this.loadResults();
break;
case 'resultsUpdating':
this.setState({
isExpectingResultsUpdate: true
});
break;
default:
assertNever(msg);
}
}
private updateStateWithNewResultsInfo(resultsInfo: ResultsInfo): void {
this.setState(prevState => {
const stateWithDisplayedResults = (displayedResults: ResultsState) => ({
displayedResults,
isExpectingResultsUpdate: prevState.isExpectingResultsUpdate,
nextResultsInfo: resultsInfo
});
if (!prevState.isExpectingResultsUpdate && resultsInfo === null) {
// No results to display
return stateWithDisplayedResults({
resultsInfo: null,
results: null,
errorMessage: 'No results to display'
});
}
if (!resultsInfo || !resultsInfo.shouldKeepOldResultsWhileRendering) {
// Display loading message
return stateWithDisplayedResults({
resultsInfo: null,
results: null,
errorMessage: 'Loading results…'
});
}
return stateWithDisplayedResults(prevState.displayedResults);
});
}
private async loadResults(): Promise<void> {
const resultsInfo = this.state.nextResultsInfo;
if (resultsInfo === null) {
return;
}
let results: Results | null = null;
let statusText: string = '';
try {
results = {
resultSets: await this.getResultSets(resultsInfo),
database: resultsInfo.database,
sortStates: this.getSortStates(resultsInfo)
};
}
catch (e) {
let errorMessage: string;
if (e instanceof Error) {
errorMessage = e.message;
} else {
errorMessage = 'Unknown error';
}
statusText = `Error loading results: ${errorMessage}`;
}
this.setState(prevState => {
// Only set state if this results info is still current.
if (resultsInfo !== prevState.nextResultsInfo) {
return null;
}
return {
displayedResults: {
resultsInfo,
results,
errorMessage: statusText
},
nextResultsInfo: null,
isExpectingResultsUpdate: false
}
});
}
private async getResultSets(resultsInfo: ResultsInfo): Promise<readonly ResultSet[]> {
const unsortedResponse = await fetch(resultsInfo.resultsPath);
const unsortedResultSets = await parseResultSets(unsortedResponse);
return Promise.all(unsortedResultSets.map(async unsortedResultSet => {
const sortedResultSetInfo = resultsInfo.sortedResultsMap.get(unsortedResultSet.schema.name);
if (sortedResultSetInfo === undefined) {
return unsortedResultSet;
}
const response = await fetch(sortedResultSetInfo.resultsPath);
const resultSets = await parseResultSets(response);
if (resultSets.length != 1) {
throw new Error(`Expected sorted BQRS to contain a single result set, encountered ${resultSets.length} result sets.`);
}
return resultSets[0];
}));
}
private getSortStates(resultsInfo: ResultsInfo): Map<string, SortState> {
const entries = Array.from(resultsInfo.sortedResultsMap.entries());
return new Map(entries.map(([key, sortedResultSetInfo]) =>
[key, sortedResultSetInfo.sortState]));
}
render() {
const displayedResults = this.state.displayedResults;
if (displayedResults.results !== null) {
return <ResultTables rawResultSets={displayedResults.results.resultSets}
interpretation={displayedResults.resultsInfo ? displayedResults.resultsInfo.interpretation : undefined}
database={displayedResults.results.database}
resultsPath={displayedResults.resultsInfo ? displayedResults.resultsInfo.resultsPath : undefined}
kind={displayedResults.resultsInfo ? displayedResults.resultsInfo.kind : undefined}
sortStates={displayedResults.results.sortStates}
isLoadingNewResults={this.state.isExpectingResultsUpdate || this.state.nextResultsInfo !== null} />;
}
else {
return <span>{displayedResults.errorMessage}</span>;
}
}
componentDidMount() {
this.vscodeMessageHandler = evt => this.handleMessage(evt.data as IntoResultsViewMsg);
window.addEventListener('message', this.vscodeMessageHandler);
}
componentWillUnmount() {
if (this.vscodeMessageHandler) {
window.removeEventListener('message', this.vscodeMessageHandler);
}
}
private vscodeMessageHandler: ((ev: MessageEvent) => void) | undefined = undefined;
}
Rdom.render(
<App />,
document.getElementById('root')
);
vscode.postMessage({ t: "resultViewLoaded" })

View File

@@ -0,0 +1,140 @@
.vscode-codeql__result-table {
display: none;
border-collapse: collapse;
width: 100%;
}
.vscode-codeql__result-table--selected {
display: table;
}
.vscode-codeql__table-selection-header {
display: flex;
padding: 0.5em 0;
}
.vscode-codeql__table-selection-header select {
border: 0;
}
.vscode-codeql__result-table-toggle-diagnostics {
display: none;
text-align: left;
margin-left: auto;
}
.vscode-codeql__result-table-toggle-diagnostics--selected {
display: inline-block;
}
/* Keep the checkbox and its label in horizontal alignment. */
.vscode-codeql__result-table-toggle-diagnostics--selected label,
.vscode-codeql__result-table-toggle-diagnostics--selected input {
display: inline-block;
vertical-align: middle;
}
.vscode-codeql__result-table-toggle-diagnostics--selected input {
margin: 3px 3px 1px 3px;
}
.vscode-codeql__result-table th {
border-top: 1px solid rgba(88,96,105,0.25);
border-bottom: 1px solid rgba(88,96,105,0.25);
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
background: rgba(225,228,232, 0.25);
padding: 0.25em 0.5em;
text-align: center;
font-weight: normal;
opacity: 0.6;
}
.vscode-codeql__result-table .sort-none::after {
/* Want to take up the same space as the other sort directions */
content: " ▲";
visibility: hidden;
}
.vscode-codeql__result-table .sort-asc::after {
content: " ▲";
}
.vscode-codeql__result-table .sort-desc::after {
content: " ▼";
}
.vscode-codeql__result-table td {
margin: 0;
padding: 0;
padding: 0.25em 0.5em;
white-space: pre-wrap;
text-align: left;
}
.vscode-codeql__result-table-location-link {
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
}
select {
background-color: var(--vscode-dropdown-background);
color: var(--vscode-dropdown-foreground);
border-color: var(--vscode-dropdown-border);
font-family: inherit;
font-size: inherit;
}
.vscode-codeql__result-tables-updating-text {
margin-left: 1em;
}
.vscode-codeql__result-table-row--odd {
background-color: inherit;
}
.vscode-codeql__result-table-row--even {
background-color: var(--vscode-textBlockQuote-background);
}
td.vscode-codeql__icon-cell {
text-align: center;
position: relative;
white-space: nowrap;
margin: 0;
padding: 0;
width: 24px;
}
td.vscode-codeql__dropdown-cell:hover {
cursor: pointer;
}
td.vscode-codeql__path-index-cell {
text-align: right;
}
td.vscode-codeql__location-cell {
text-align: right;
}
.vscode-codeql__vertical-rule {
border-left: 1px solid var(--vscode-dropdown-border);
height: 100%;
position: absolute;
top: 0;
bottom: 0;
}
.vscode-codeql__title {
/* Something that isn't a link, but which has a title attribute */
text-decoration-line: underline;
text-decoration-style: dotted;
}
.octicon {
fill: var(--vscode-editor-foreground);
margin-top: .25em;
}
.octicon-light {
opacity: 0.6;
}

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"target": "es6",
"outDir": "out",
"lib": [
"es6",
"dom"
],
"jsx": "react",
"sourceMap": true,
"rootDir": "..",
"strict": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true
},
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,61 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
/**
* Helper function that runs all Mocha tests found in the
* given test root directory.
*
* For each integration test suite, `vscode-test` expects
* a test runner script exporting a function with the signature:
* ```ts
* export function run(): Promise<void>
* ```
*
* To create an integration test suite:
* - create a directory beside this file
* - create integration tests in the directory, named `<name>.test.ts`
* - create an `index.ts` file in the directory, containing:
* ```ts
* import { runTestsInDirectory } from '../index-template';
* export function run(): Promise<void> {
* return runTestsInDirectory(__dirname);
* }
* ```
*
* After https://github.com/microsoft/TypeScript/issues/420 is implemented,
* this pattern can be expressed more neatly using a module interface.
*/
export function runTestsInDirectory(testsRoot: string): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'bdd'
});
mocha.useColors(true);
return new Promise((c, e) => {
console.log(`Adding test cases from ${testsRoot}`);
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
e(err);
}
});
});
}

View File

@@ -0,0 +1,26 @@
import * as assert from 'assert';
import * as path from 'path';
import * as vscode from 'vscode';
describe('launching with a minimal workspace', async () => {
const ext = vscode.extensions.getExtension('GitHub.vscode-codeql');
it('should install the extension', () => {
assert(ext);
});
it('should not activate the extension at first', () => {
assert(ext!.isActive === false);
});
it('should activate the extension when a .ql file is opened', async function () {
const folders = vscode.workspace.workspaceFolders;
assert(folders && folders.length === 1);
const folderPath = folders![0].uri.fsPath;
const documentPath = path.resolve(folderPath, 'query.ql');
const document = await vscode.workspace.openTextDocument(documentPath);
assert(document.languageId === 'ql');
// Delay slightly so that the extension has time to activate.
this.timeout(3000);
setTimeout(() => {
assert(ext!.isActive);
}, 1000);
});
});

View File

@@ -0,0 +1,4 @@
import { runTestsInDirectory } from '../index-template';
export function run(): Promise<void> {
return runTestsInDirectory(__dirname);
}

View File

@@ -0,0 +1,13 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
// Note that this may open the most recent VSCode workspace.
describe('launching with no specified workspace', () => {
const ext = vscode.extensions.getExtension('GitHub.vscode-codeql');
it('should install the extension', () => {
assert(ext);
});
it('should not activate the extension at first', () => {
assert(ext!.isActive === false);
});
});

View File

@@ -0,0 +1,38 @@
import { expect } from "chai";
import * as path from "path";
import { ArchiveFileSystemProvider, decodeSourceArchiveUri, encodeSourceArchiveUri, ZipFileReference } from "../../archive-filesystem-provider";
describe("archive filesystem provider", () => {
it("reads empty file correctly", async () => {
const archiveProvider = new ArchiveFileSystemProvider();
const uri = encodeSourceArchiveUri({
sourceArchiveZipPath: path.resolve(__dirname, "data/archive-filesystem-provider-test/single_file.zip"),
pathWithinSourceArchive: "/aFileName.txt"
});
const data = await archiveProvider.readFile(uri);
expect(data.length).to.equal(0);
});
});
describe('source archive uri encoding', function () {
const testCases: { name: string, input: ZipFileReference }[] = [
{
name: 'mixed case and unicode',
input: { sourceArchiveZipPath: "/I-\u2665-codeql.zip", pathWithinSourceArchive: "/foo/bar" }
},
{
name: 'Windows path',
input: { sourceArchiveZipPath: "C:/Users/My Name/folder/src.zip", pathWithinSourceArchive: "/foo/bar.ext" }
},
{
name: 'Unix path',
input: { sourceArchiveZipPath: "/home/folder/src.zip", pathWithinSourceArchive: "/foo/bar.ext" }
}
];
for (const testCase of testCases) {
it(`should work round trip with ${testCase.name}`, function () {
const output = decodeSourceArchiveUri(encodeSourceArchiveUri(testCase.input));
expect(output).to.eql(testCase.input);
});
}
});

View File

@@ -0,0 +1,50 @@
import { expect } from "chai";
import "mocha";
import { tryParseVersionString } from "../../cli-version";
describe("Version parsing", () => {
it("should accept version without prerelease and build metadata", () => {
const v = tryParseVersionString("3.2.4")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.be.undefined;
});
it("should accept v at the beginning of the version", () => {
const v = tryParseVersionString("v3.2.4")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.be.undefined;
});
it("should accept version with prerelease", () => {
const v = tryParseVersionString("v3.2.4-alpha.0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.equal("alpha.0");
expect(v.buildMetadata).to.be.undefined;
});
it("should accept version with prerelease and build metadata", () => {
const v = tryParseVersionString("v3.2.4-alpha.0+abcdef0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.equal("alpha.0");
expect(v.buildMetadata).to.equal("abcdef0");
});
it("should accept version with build metadata", () => {
const v = tryParseVersionString("v3.2.4+abcdef0")!;
expect(v.majorVersion).to.equal(3);
expect(v.minorVersion).to.equal(2);
expect(v.patchVersion).to.equal(4);
expect(v.prereleaseVersion).to.be.undefined;
expect(v.buildMetadata).to.equal("abcdef0");
});
});

View File

@@ -0,0 +1,178 @@
import { expect } from "chai";
import * as fetch from "node-fetch";
import "mocha";
import { Version } from "../../cli-version";
import { GithubRelease, GithubReleaseAsset, ReleasesApiConsumer, versionCompare } from "../../distribution"
describe("Releases API consumer", () => {
const owner = "someowner";
const repo = "somerepo";
const sampleReleaseResponse: GithubRelease[] = [
{
"assets": [],
"created_at": "2019-09-01T00:00:00Z",
"id": 1,
"name": "",
"prerelease": false,
"tag_name": "v2.1.0"
},
{
"assets": [],
"created_at": "2019-08-10T00:00:00Z",
"id": 2,
"name": "",
"prerelease": false,
"tag_name": "v3.1.1"
},
{
"assets": [],
"created_at": "2019-09-05T00:00:00Z",
"id": 3,
"name": "",
"prerelease": false,
"tag_name": "v2.0.0"
},
{
"assets": [],
"created_at": "2019-08-11T00:00:00Z",
"id": 4,
"name": "",
"prerelease": true,
"tag_name": "v3.1.2-pre"
},
];
const unconstrainedVersionConstraint = {
description: "*",
isVersionCompatible: () => true
};
it("picking latest release: is based on version", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
}
}
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint);
expect(latestRelease.id).to.equal(2);
});
it("picking latest release: obeys version constraints", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
}
}
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease({
description: "2.*.*",
isVersionCompatible: version => version.majorVersion === 2
});
expect(latestRelease.id).to.equal(1);
});
it("picking latest release: includes prereleases when option set", async () => {
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
return Promise.resolve(new fetch.Response(JSON.stringify(sampleReleaseResponse)));
}
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
}
}
const consumer = new MockReleasesApiConsumer(owner, repo);
const latestRelease = await consumer.getLatestRelease(unconstrainedVersionConstraint, true);
expect(latestRelease.id).to.equal(4);
});
it("gets correct assets for a release", async () => {
const expectedAssets: GithubReleaseAsset[] = [
{
"id": 1,
"name": "firstAsset",
"size": 11
},
{
"id": 2,
"name": "secondAsset",
"size": 12
}
];
class MockReleasesApiConsumer extends ReleasesApiConsumer {
protected async makeApiCall(apiPath: string, additionalHeaders: { [key: string]: string } = {}): Promise<fetch.Response> {
if (apiPath === `/repos/${owner}/${repo}/releases`) {
const responseBody: GithubRelease[] = [{
"assets": expectedAssets,
"created_at": "2019-09-01T00:00:00Z",
"id": 1,
"name": "Release 1",
"prerelease": false,
"tag_name": "v2.0.0"
}];
return Promise.resolve(new fetch.Response(JSON.stringify(responseBody)));
}
return Promise.reject(new Error(`Unknown API path: ${apiPath}`));
}
}
const consumer = new MockReleasesApiConsumer(owner, repo);
const assets = (await consumer.getLatestRelease(unconstrainedVersionConstraint)).assets;
expect(assets.length).to.equal(expectedAssets.length);
expectedAssets.map((expectedAsset, index) => {
expect(assets[index].id).to.equal(expectedAsset.id);
expect(assets[index].name).to.equal(expectedAsset.name);
expect(assets[index].size).to.equal(expectedAsset.size);
});
});
});
describe("Release version ordering", () => {
function createVersion(majorVersion: number, minorVersion: number, patchVersion: number, prereleaseVersion?: string, buildMetadata?: string): Version {
return {
buildMetadata,
majorVersion,
minorVersion,
patchVersion,
prereleaseVersion,
rawString: `${majorVersion}.${minorVersion}.${patchVersion}` +
prereleaseVersion ? `-${prereleaseVersion}` : "" +
buildMetadata ? `+${buildMetadata}` : ""
};
}
it("major versions compare correctly", () => {
expect(versionCompare(createVersion(3, 0, 0), createVersion(2, 9, 9)) > 0).to.be.true;
});
it("minor versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 0), createVersion(2, 0, 9)) > 0).to.be.true;
});
it("patch versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 2), createVersion(2, 1, 1)) > 0).to.be.true;
});
it("prerelease versions compare correctly", () => {
expect(versionCompare(createVersion(2, 1, 0, "alpha.2"), createVersion(2, 1, 0, "alpha.1")) > 0).to.true;
});
it("build metadata compares correctly", () => {
expect(versionCompare(createVersion(2, 1, 0, "alpha.1", "abcdef0"), createVersion(2, 1, 0, "alpha.1", "bcdef01"))).to.equal(0);
});
});

View File

@@ -0,0 +1,4 @@
import { runTestsInDirectory } from '../index-template';
export function run(): Promise<void> {
return runTestsInDirectory(__dirname);
}

View File

@@ -0,0 +1,34 @@
import { expect } from "chai";
import * as tmp from "tmp";
import { window, ViewColumn, Uri } from "vscode";
import { fileUriToWebviewUri, webviewUriToFileUri } from '../../interface';
describe('webview uri conversion', function () {
it('should correctly round trip from filesystem to webview and back', function () {
const tmpFile = tmp.fileSync({ prefix: 'uri_test_', postfix: '.bqrs', keep: false });
const fileUriOnDisk = Uri.file(tmpFile.name);
const panel = window.createWebviewPanel(
'test panel',
'test panel',
ViewColumn.Beside,
{
enableScripts: false,
localResourceRoots: [
fileUriOnDisk
]
}
);
after(function () {
panel.dispose();
tmpFile.removeCallback();
});
// CSP allowing nothing, to prevent warnings.
const html = `<html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none';"></head></html>`;
panel.webview.html = html;
const webviewUri = fileUriToWebviewUri(panel, fileUriOnDisk);
const reconstructedFileUri = webviewUriToFileUri(webviewUri);
expect(reconstructedFileUri.toString(true)).to.equal(fileUriOnDisk.toString(true));
});
});

View File

@@ -0,0 +1,44 @@
import * as path from 'path';
import { runTests } from 'vscode-test';
/**
* Integration test runner. Launches the VSCode Extension Development Host with this extension installed.
* See https://github.com/microsoft/vscode-test/blob/master/sample/test/runTest.ts
*/
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`.
const extensionDevelopmentPath = path.resolve(__dirname, '../..');
// List of integration test suites.
// The path to the extension test runner script is passed to --extensionTestsPath.
const integrationTestSuites = [
// Tests with no workspace selected upon launch.
{
extensionDevelopmentPath: extensionDevelopmentPath,
extensionTestsPath: path.resolve(__dirname, 'no-workspace', 'index'),
launchArgs: ['--disable-extensions'],
},
// Tests with a simple workspace selected upon launch.
{
extensionDevelopmentPath: extensionDevelopmentPath,
extensionTestsPath: path.resolve(__dirname, 'minimal-workspace', 'index'),
launchArgs: [
path.resolve(__dirname, '../../test/data'),
'--disable-extensions',
]
}
];
for (const integrationTestSuite of integrationTestSuites) {
// Download and unzip VS Code if necessary, and run the integration test suite.
await runTests(integrationTestSuite);
}
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,353 @@
---
# This file is transformed into the equivalent JSON TextMate grammar, with the following additional
# features available:
#
# The `regexOptions` Property
# A top-level property named `regexOptions` may be defined with a string value. This string
# represents the set of regular expression options to apply to all regular expressions throughout
# the file.
#
# Macros
# The `macros` element defines a map of macro names to replacement text. When a `match`, `begin`, or
# `end` property has a value that is a single-key map, the value is replaced with the value of the
# macro named by the key, with any use of `(?#)` in the macro text replaced with the text of the
# value of the key, surrounded by a non-capturing group (`(?:)`). For example:
#
# The `beginPattern` and `endPattern` Properties
# A rule can have a `beginPattern` or `endPattern` property whose value is a reference to another
# rule (e.g. `#other-rule`). The `beginPattern` property is replaced as follows:
#
# my-rule:
# beginPattern: '#other-rule'
#
# would be transformed to
#
# my-rule:
# begin: '(?#other-rule)'
# beginCaptures:
# '0':
# patterns:
# - include: '#other-rule'
#
# An `endPattern` property is transformed similary.
#
# macros:
# repeat: '(?#)*'
# repository:
# multi-letter:
# match:
# repeat: '[A-Za-z]'
# name: scope.multi-letter
#
# would be transformed to
#
# repository:
# multi-letter:
# match: '(?:[A-Za-z])*'
# name: scope.multi-letter
#
# Reference Expansion
# Any comment of the form `(?#ref-id)` in a `match`, `begin`, or `end` property will be replaced
# with the match text of the rule named "ref-id". If the rule named "ref-id" consists of just a
# `patterns` property with a list of `include` directives, the replacement pattern is the
# disjunction of the match patterns of all of the included rules.
name: DBScheme
scopeName: source.dbscheme
fileTypes: [dbscheme]
uuid: BE08929D-CEAC-4B88-9844-57475F4E8A82
regexOptions: 'x' # Ignore pattern whitespace
# Macros are parameterized patterns that can be used as a match elsewhere in the file.
# To use a macro, replace the string for a `match`, `begin`, or `end` property with a single-element
# map whose key is the name of the macro to invoke, and whose value is a string to be substituted for
# any usage of `(?#)` in the macro pattern definition.
macros:
keyword: '\b(?#)(?#end-of-id)'
annotation: '\#(?#)(?#end-of-id)'
patterns:
- include: '#table-column-list'
- include: '#case-declaration-head'
- include: '#annotation'
- include: '#non-context-sensitive'
- include: '#table-name'
- include: '#type-name'
repository:
# A character that can appear somewhere in an identifier.
id-letter:
match: '[0-9A-Za-z_]'
# Matches a position containing a non-identifier character. Used to ensure we do not match partial
# identifiers/keywords in other rules.
end-of-id:
match: '(?!(?#id-letter))'
id:
match: '\b [A-Za-z][0-9A-Za-z_]* (?#end-of-id)'
at-id:
match: '@[A-Za-z][0-9A-Za-z_]* (?#end-of-id)'
# An integer literal.
integer-literal:
match: '[0-9]+(?![0-9])'
name: constant.numeric.decimal.dbscheme
# A pattern that can start a comment.
comment-start:
match: '// | /\*'
# A QL comment, regardless of form.
comment:
patterns:
# A QLDoc (`/** */`) comment.
- begin: '/\*\*'
end: '\*/'
name: comment.block.documentation.dbscheme
# Highlight tag names within the QLDoc.
patterns:
- begin: '(?<=/\*\*)([^*]|\*(?!/))*$'
while: '(^|\G)\s*\*(?!/)(?=([^*]|[*](?!/))*$)'
patterns:
- include: 'text.html.markdown#fenced_code_block'
- include: 'text.html.markdown#lists'
- include: 'text.html.markdown#inline'
- match: '\G\s* (@\S+)'
name: keyword.tag.dbscheme
# A block (`/* */`) comment.
- begin: '/\*'
end: '\*/'
name: comment.block.dbscheme
# A single line (`//`) comment.
- match: //.*$
name: comment.line.double-slash.dbscheme
# Operators and punctuation
sub:
match: '<:'
name: punctuation.sub.sub.dbscheme
pipe:
match: '\|'
name: punctuation.separator.pipe.dbscheme
open-paren:
match: '\('
name: punctuation.parenthesis.open.dbscheme
close-paren:
match: '\)'
name: punctuation.parenthesis.close.dbscheme
semicolon:
match: ';'
name: punctuation.separator.statement.dbscheme
colon:
match: ':'
name: punctuation.separator.colon.dbscheme
comma:
match: ','
name: punctuation.separator.comma.dbscheme
equals:
match: '='
name: punctuation.separator.equals.dbscheme
dot:
match: '\.'
name: punctuation.accessor.dbscheme
open-bracket:
match: '\['
name: punctuation.squarebracket.open.dbscheme
close-bracket:
match: '\]'
name: punctuation.squarebracket.close.dbscheme
operator-or-punctuation:
patterns:
- include: '#sub'
- include: '#pipe'
- include: '#open-paren'
- include: '#close-paren'
- include: '#semicolon'
- include: '#colon'
- include: '#comma'
- include: '#equals'
- include: '#dot'
- include: '#open-bracket'
- include: '#close-bracket'
# Annotations
keyset:
match:
annotation: 'keyset'
name: storage.modifier.keyset.dbscheme
computed:
match:
annotation: 'computed'
name: storage.modifier.computed.dbscheme
annotation-keyword:
patterns:
- include: '#keyset'
- include: '#computed'
# Keywords
type:
match:
keyword: 'type'
name: keyword.other.type.dbscheme
subtype:
match:
keyword: 'subtype'
name: keyword.other.subtype.dbscheme
case:
match:
keyword: 'case'
name: keyword.other.case.dbscheme
of:
match:
keyword: 'of'
name: keyword.other.of.dbscheme
order:
match:
keyword: 'order'
name: keyword.other.order.dbscheme
key:
match:
keyword: 'key'
name: keyword.other.key.dbscheme
ref:
match:
keyword: 'ref'
name: storage.modifier.ref.dbscheme
int:
match:
keyword: 'int'
name: keyword.type.boolean.dbscheme
float:
match:
keyword: 'float'
name: keyword.type.float.dbscheme
boolean:
match:
keyword: 'boolean'
name: keyword.type.boolean.dbscheme
date:
match:
keyword: 'date'
name: keyword.type.date.dbscheme
varchar:
match:
keyword: 'varchar'
name: keyword.type.varchar.dbscheme
string:
match:
keyword: 'string'
name: keyword.type.string.dbscheme
unique:
match:
keyword: 'unique'
name: storage.modifier.unique.dbscheme
# Any "true" keyword (not including annotations).
keyword:
patterns:
- include: '#type'
- include: '#subtype'
- include: '#case'
- include: '#of'
- include: '#order'
- include: '#key'
- include: '#ref'
- include: '#int'
- include: '#float'
- include: '#boolean'
- include: '#date'
- include: '#varchar'
- include: '#string'
- include: '#unique'
# All tokens that can appear in any context.
non-context-sensitive:
patterns:
- include: '#comment'
- include: '#integer-literal'
- include: '#operator-or-punctuation'
- include: '#keyword'
# An annotation on a table declaration.
annotation:
patterns:
- include: '#keyset-annotation'
- include: '#annotation-keyword'
# A `#keyset` annotation, including its arguments.
keyset-annotation:
beginPattern: '#keyset'
# Ends after the next `]`, or when we encounter something other than a `[`.
end: '(?! \s | (?#comment-start) | \[ ) |
(?<=\])'
name: meta.block.keyset-annotation.dbscheme
patterns:
- include: '#keyset-annotation-body'
- include: '#non-context-sensitive'
# The bracket-enclosed body of a `#keyset` annotation.
keyset-annotation-body:
beginPattern: '#open-bracket'
endPattern: '#close-bracket'
name: meta.block.keyset-annotation-body.dbscheme
patterns:
- include: '#non-context-sensitive'
- include: '#column-name'
table-column-list:
beginPattern: '#open-paren'
endPattern: '#close-paren'
name: meta.block.table-column-list.dbscheme
patterns:
- include: '#non-context-sensitive'
- include: '#column-name'
- include: '#type-name'
case-declaration-head:
beginPattern: '#case'
end: '(?!\s|(?#id)|(?#at-id)|(?#dot)|(?#comment-start))'
name: meta.block.case-declaration-head.dbscheme
patterns:
- include: '#non-context-sensitive'
- include: '#column-name'
- include: '#type-name'
column-name:
match: '(?#id)'
name: entity.name.variable.parameter.dbscheme
type-name:
match: '(?#at-id)'
name: entity.name.type.dbscheme
table-name:
match: '(?#id)'
name: entity.name.function.dbscheme

File diff suppressed because it is too large Load Diff

2
extensions/ql-vscode/test/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
test-db
default

View File

@@ -0,0 +1,4 @@
query predicate edges(int i, int j) {
i = 1 and j = 2 or i = 2 and j = 3
}
select "s"

View File

@@ -0,0 +1 @@
select 42, 3.14159, "hello world", true

View File

@@ -0,0 +1,4 @@
<dbstats>
<typesizes> </typesizes>
<stats> </stats>
</dbstats>

View File

@@ -0,0 +1,32 @@
import { expect } from 'chai';
import 'mocha';
import { LocationStyle, StringLocation, tryGetWholeFileLocation } from 'semmle-bqrs';
describe('processing string locations', function () {
it('should detect Windows whole-file locations', function () {
const loc: StringLocation = {
t: LocationStyle.String,
loc: 'file://C:/path/to/file.ext:0:0:0:0'
};
const wholeFileLoc = tryGetWholeFileLocation(loc);
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: 'C:/path/to/file.ext'});
});
it('should detect Unix whole-file locations', function () {
const loc: StringLocation = {
t: LocationStyle.String,
loc: 'file:///path/to/file.ext:0:0:0:0'
};
const wholeFileLoc = tryGetWholeFileLocation(loc);
expect(wholeFileLoc).to.eql({t: LocationStyle.WholeFile, file: '/path/to/file.ext'});
});
it('should ignore other string locations', function () {
for (const loc of ['file:///path/to/file.ext', 'I am not a location']) {
const wholeFileLoc = tryGetWholeFileLocation({
t: LocationStyle.String,
loc: loc
});
expect(wholeFileLoc).to.be.undefined;
}
});
});

View File

@@ -0,0 +1,227 @@
import { expect } from 'chai';
import * as fs from 'fs-extra';
import 'mocha';
import * as path from 'path';
import * as bqrs from 'semmle-bqrs';
import { FileReader } from 'semmle-io-node';
import * as tmp from 'tmp';
import * as url from 'url';
import { CancellationTokenSource } from 'vscode-jsonrpc';
import * as messages from '../../src/messages';
import * as qsClient from '../../src/queryserver-client';
import * as cli from '../../src/cli';
import { ProgressReporter } from '../../src/logging';
declare module "url" {
export function pathToFileURL(urlStr: string): Url;
}
const tmpDir = tmp.dirSync({ prefix: 'query_test_', keep: false, unsafeCleanup: true });
const COMPILED_QUERY_PATH = path.join(tmpDir.name, 'compiled.qlo');
const RESULTS_PATH = path.join(tmpDir.name, 'results.bqrs');
const source = new CancellationTokenSource();
const token = source.token;
class Checkpoint<T> {
private res: () => void;
private rej: (e: Error) => void;
private promise: Promise<T>;
constructor() {
this.res = () => { };
this.rej = () => { };
this.promise = new Promise((res, rej) => { this.res = res; this.rej = rej; })
}
async done() {
return this.promise;
}
async resolve() {
(this.res)();
}
async reject(e: Error) {
(this.rej)(e);
}
}
type ResultSets = {
[name: string]: bqrs.ColumnValue[][]
}
type QueryTestCase = {
queryPath: string,
expectedResultSets: ResultSets
}
// Test cases: queries to run and their expected results.
const queryTestCases: QueryTestCase[] = [
{
queryPath: path.join(__dirname, '../data/query.ql'),
expectedResultSets: {
'#select': [[42, 3.14159, "hello world", true]]
}
},
{
queryPath: path.join(__dirname, '../data/multiple-result-sets.ql'),
expectedResultSets: {
'edges': [[1, 2], [2, 3]],
'#select': [["s"]]
}
}
];
describe('using the query server', function () {
before(function () {
if (process.env["CODEQL_PATH"] === undefined) {
console.log('The environment variable CODEQL_PATH is not set. The query server tests, which require the CodeQL CLI, will be skipped.');
this.skip();
}
});
const codeQlPath = process.env["CODEQL_PATH"]!;
let qs: qsClient.QueryServerClient;
let cliServer: cli.CodeQLCliServer;
after(() => {
if (qs) {
qs.dispose();
}
if (cliServer) {
cliServer.dispose();
}
});
it('should be able to start the query server', async function () {
const consoleProgressReporter: ProgressReporter = {
report: (v: {message: string}) => console.log(`progress reporter says ${v.message}`)
};
const logger = {
log: (s: string) => console.log('logger says', s),
logWithoutTrailingNewline: (s: string) => { }
};
cliServer = new cli.CodeQLCliServer({
async getCodeQlPathWithoutVersionCheck(): Promise<string | undefined> {
return codeQlPath;
},
}, logger)
qs = new qsClient.QueryServerClient(
{
codeQlPath,
numThreads: 1,
queryMemoryMb: 1024,
timeoutSecs: 1000,
debug: false
},
cliServer,
{
logger
},
task => task(consoleProgressReporter, token)
);
await qs.startQueryServer();
});
// Note this does not work with arrow functions as the test case bodies:
// ensure they are all written with standard anonymous functions.
this.timeout(5000);
for (const queryTestCase of queryTestCases) {
const queryName = path.basename(queryTestCase.queryPath);
const compilationSucceeded = new Checkpoint<void>();
const evaluationSucceeded = new Checkpoint<void>();
it(`should be able to compile query ${queryName}`, async function () {
expect(fs.existsSync(queryTestCase.queryPath)).to.be.true;
try {
const qlProgram: messages.QlProgram = {
libraryPath: [],
dbschemePath: path.join(__dirname, '../data/test.dbscheme'),
queryPath: queryTestCase.queryPath
};
const params: messages.CompileQueryParams = {
compilationOptions: {
computeNoLocationUrls: true,
failOnWarnings: false,
fastCompilation: false,
includeDilInQlo: true,
localChecking: false,
noComputeGetUrl: false,
noComputeToString: false,
},
queryToCheck: qlProgram,
resultPath: COMPILED_QUERY_PATH,
target: { query: {} }
};
const result = await qs.sendRequest(messages.compileQuery, params, token, () => { });
expect(result.messages!.length).to.equal(0);
compilationSucceeded.resolve();
}
catch (e) {
compilationSucceeded.reject(e);
}
});
it(`should be able to run query ${queryName}`, async function () {
try {
await compilationSucceeded.done();
const callbackId = qs.registerCallback(res => {
evaluationSucceeded.resolve();
});
const queryToRun: messages.QueryToRun = {
resultsPath: RESULTS_PATH,
qlo: url.pathToFileURL(COMPILED_QUERY_PATH).toString(),
allowUnknownTemplates: true,
id: callbackId,
timeoutSecs: 1000,
};
const db: messages.Dataset = {
dbDir: path.join(__dirname, '../test-db'),
workingSet: 'default',
}
const params: messages.EvaluateQueriesParams = {
db,
evaluateId: callbackId,
queries: [queryToRun],
stopOnError: false,
useSequenceHint: false
};
await qs.sendRequest(messages.runQueries, params, token, () => { });
}
catch (e) {
evaluationSucceeded.reject(e);
}
});
const actualResultSets: ResultSets = {};
it(`should be able to parse results of query ${queryName}`, async function () {
let fileReader: FileReader | undefined;
try {
await evaluationSucceeded.done();
fileReader = await FileReader.open(RESULTS_PATH);
const resultSetsReader = await bqrs.open(fileReader);
for (const reader of resultSetsReader.resultSets) {
const actualRows: bqrs.ColumnValue[][] = [];
for await (const row of reader.readTuples()) {
actualRows.push(row);
}
actualResultSets[reader.schema.name] = actualRows;
}
} finally {
if (fileReader) {
fileReader.dispose();
}
}
});
it(`should have correct results for query ${queryName}`, async function () {
expect(actualResultSets!).not.to.be.empty;
expect(Object.keys(actualResultSets!).sort()).to.eql(Object.keys(queryTestCase.expectedResultSets).sort());
for (const name in queryTestCase.expectedResultSets) {
expect(actualResultSets![name]).to.eql(queryTestCase.expectedResultSets[name], `Results for query predicate ${name} do not match`);
}
});
}
});

View File

@@ -0,0 +1,3 @@
{
"extends": "./node_modules/typescript-config/extension.tsconfig.json"
}

View File

@@ -0,0 +1,7 @@
'use strict';
require('ts-node').register({});
const { compileTypeScript, watchTypeScript } = require('build-tasks');
exports.default = compileTypeScript;
exports.watchTypeScript = watchTypeScript;

View File

@@ -0,0 +1,32 @@
{
"name": "semmle-bqrs",
"description": "Parses Binary Query Result Sets generated by CodeQL",
"author": "GitHub",
"private": true,
"version": "0.0.1",
"publisher": "GitHub",
"repository": {
"type": "git",
"url": "https://github.com/github/vscode-codeql"
},
"main": "./out/index",
"files": [
"out/**",
"package.json"
],
"scripts": {
"build": "gulp",
"format": "tsfmt -r"
},
"dependencies": {
"leb": "^0.3.0",
"reflect-metadata": "~0.1.13",
"semmle-io": "^0.0.1"
},
"devDependencies": {
"@types/node": "^12.0.8",
"build-tasks": "^0.0.1",
"typescript-config": "^0.0.1",
"typescript-formatter": "^7.2.2"
}
}

View File

@@ -0,0 +1,407 @@
import { ResultSetSchema, LocationStyle, ColumnTypeKind } from "./bqrs-schema";
import { ResultSetsReader, ResultSetReader } from "./bqrs-file";
import { ElementBase, ColumnValue } from "./bqrs-results";
/**
* Represents a binding to all remaining columns, starting at the column index specified by
* `startColumn`.
*/
export interface RestColumnIndex {
startColumn: number
}
/**
* Indentifies the result column to which a property is bound. May be the index of a specific
* column, or an instance of `RestColumnIndex` to bind to all remaining columns.
*/
export type ColumnIndex = number | RestColumnIndex;
/**
* Options that can be specified for a `@qlTable` attribute.
*/
export interface TableOptions {
/**
* The name of the table to bind to. If multiple values are specified, the property is bound to
* the the table whose name is earliest in the list.
*/
name?: string | string[];
}
export enum QLOption {
Required = 'required',
Optional = 'optional',
Forbidden = 'forbidden'
}
/**
* Options that can be specified for a `@qlElement` attribute.
*/
export interface ElementOptions {
label?: QLOption;
location?: QLOption;
}
/**
* An attribute that binds the target property to a result column representing a QL element.
* @param index Index of the column to be bound.
* @param options Binding options.
*/
export function qlElement(index: ColumnIndex, options: ElementOptions = {}): PropertyDecorator {
return (proto: any, key: PropertyKey): void => {
column(proto, {
key: key,
index: index,
type: 'e',
options: {
label: options.label ? options.label : QLOption.Required,
location: options.location ? options.location : QLOption.Required
}
});
}
}
/**
* An attribute that binds the target property to a result column containing a QL string.
* @param index Index of the column to be bound.
*/
export function qlString(index: ColumnIndex): PropertyDecorator {
return (proto: any, key: PropertyKey): void => {
column(proto, {
key: key,
index: index,
type: 's'
});
}
}
/**
* An attribute that binds the target property to a set of result columns. The individual
* columns are bound to properties of the underlying type of the target property.
* @param index Index of the first column to be bound.
* @param type The type of the property.
*/
export function qlTuple(index: ColumnIndex, type: { new(): any }): PropertyDecorator {
return (proto: any, key: PropertyKey): void => {
column(proto, {
key: key,
index: index,
type: type
});
}
}
type PropertyKey = string | symbol;
interface ColumnProperty {
key: PropertyKey;
index: ColumnIndex;
type: ColumnTypeKind | { new(): any };
}
interface ElementProperty extends ColumnProperty {
type: 'e';
options: Required<ElementOptions>;
}
function isElement(property: ColumnProperty): property is ElementProperty {
return property.type === 'e';
}
const columnPropertiesSymbol = Symbol('columnProperties');
type PropertyDecorator = (proto: any, key: PropertyKey) => void;
function column<T extends ColumnProperty>(proto: any, property: T): void {
let columnProperties: ColumnProperty[] | undefined = Reflect.getMetadata(columnPropertiesSymbol, proto);
if (columnProperties === undefined) {
columnProperties = [];
Reflect.defineMetadata(columnPropertiesSymbol, columnProperties, proto);
}
columnProperties.push(property);
}
interface TableProperty {
key: PropertyKey;
tableNames: string[];
rowType: any;
}
const tablePropertiesSymbol = Symbol('tableProperties');
/**
* An attribute that binds the target property to the contents of a result table.
* @param rowType The type representing a single row in the bound table. The type of the target
* property must be an array of this type.
* @param options Binding options.
*/
export function qlTable(rowType: any, options?: TableOptions): any {
return (proto, key: PropertyKey) => {
const realOptions = options || {};
let names: string[];
if (realOptions.name === undefined) {
names = [key.toString()]
}
else if (typeof realOptions.name === 'string') {
names = [realOptions.name];
}
else {
names = realOptions.name;
}
let tableProperties: TableProperty[] | undefined = Reflect.getMetadata(tablePropertiesSymbol, proto);
if (tableProperties === undefined) {
tableProperties = [];
Reflect.defineMetadata(tablePropertiesSymbol, tableProperties, proto);
}
tableProperties.push({
key: key,
tableNames: names,
rowType: rowType
});
};
}
type ParseTupleAction = (src: readonly ColumnValue[], dest: any) => void;
type TupleParser<T> = (src: readonly ColumnValue[]) => T;
export class CustomResultSet<TTuple> {
public constructor(private reader: ResultSetReader, private readonly type: { new(): TTuple },
private readonly tupleParser: TupleParser<TTuple>) {
}
public async* readTuples(): AsyncIterableIterator<TTuple> {
for await (const tuple of this.reader.readTuples()) {
yield this.tupleParser(tuple);
}
}
}
class CustomResultSetBinder {
private readonly boundColumns: boolean[];
private constructor(private readonly rowType: { new(): any },
private readonly schema: ResultSetSchema) {
this.boundColumns = Array(schema.columns.length).fill(false);
}
public static bind<TTuple>(reader: ResultSetReader, rowType: { new(): TTuple }):
CustomResultSet<TTuple> {
const binder = new CustomResultSetBinder(rowType, reader.schema);
const tupleParser = binder.bindRoot<TTuple>();
return new CustomResultSet<TTuple>(reader, rowType, tupleParser);
}
private bindRoot<TTuple>(): TupleParser<TTuple> {
const { action } = this.bindObject(this.rowType, 0, true);
const unboundColumnIndex = this.boundColumns.indexOf(false);
if (unboundColumnIndex >= 0) {
throw new Error(`Column '${this.schema.name}[${unboundColumnIndex}]' is not bound to a property.`);
}
return tuple => {
let result = new this.rowType;
action(tuple, result);
return result;
}
}
private checkElementProperty(index: ColumnIndex, propertyName: 'location' | 'label',
hasProperty: boolean, expectsProperty: QLOption): void {
switch (expectsProperty) {
case QLOption.Required:
if (!hasProperty) {
throw new Error(`Element column '${this.schema.name}[${index}]' does not have the required '${propertyName}' property.`);
}
break;
case QLOption.Forbidden:
if (!hasProperty) {
throw new Error(`Element column '${this.schema.name}[${index}]' has unexpected '${propertyName}' property.`);
}
break;
case QLOption.Optional:
break;
}
}
private bindObject(type: { new(): any }, startIndex: number, isRoot: boolean): {
action: ParseTupleAction,
lastColumn: number
} {
const columnProperties: ColumnProperty[] | undefined =
Reflect.getMetadata(columnPropertiesSymbol, type.prototype);
if (columnProperties === undefined) {
throw new Error(`Type '${type.toString()}' does not have any properties decorated with '@column'.`);
}
const actions: ParseTupleAction[] = [];
let restProperty: ColumnProperty | undefined = undefined;
let lastColumn = startIndex;
for (const property of columnProperties) {
if (typeof property.index === 'object') {
if (!isRoot) {
throw new Error(`Type '${type.toString()}' has a property bound to '...', but is not the root type.`);
}
if (restProperty !== undefined) {
throw new Error(`Type '${type.toString()}' has multiple properties bound to '...'.`);
}
restProperty = property;
}
else {
const index = property.index + startIndex;
const { action, lastColumn: lastChildColumn } = this.bindColumn(index, type, property,
property.key);
actions.push(action);
lastColumn = Math.max(lastColumn, lastChildColumn);
}
}
if (restProperty !== undefined) {
const startIndex = (<RestColumnIndex>restProperty.index).startColumn;
let index = startIndex;
let elementIndex = 0;
const elementActions: ParseTupleAction[] = [];
while (index < this.schema.columns.length) {
const { action, lastColumn: lastChildColumn } = this.bindColumn(index, type, restProperty, elementIndex);
elementActions.push(action);
index = lastChildColumn + 1;
elementIndex++;
}
const key = restProperty.key;
actions.push((src, dest) => {
const destArray = Array(elementActions.length);
elementActions.forEach(action => action(src, destArray));
dest[key] = destArray;
});
}
return {
action: (src, dest) => actions.forEach(action => action(src, dest)),
lastColumn: lastColumn
};
}
private bindColumn(index: number, type: new () => any, property: ColumnProperty,
key: PropertyKey | number): {
action: ParseTupleAction,
lastColumn: number
} {
if ((index < 0) || (index >= this.schema.columns.length)) {
throw new Error(`No matching column '${index}' found for property '${type.toString()}.${property.key.toString()}' when binding root type '${this.rowType.toString()}'.`);
}
if (typeof property.type === 'string') {
// This property is bound to a single column
return {
action: this.bindSingleColumn(index, property, type, key),
lastColumn: index
};
}
else {
// This property is a tuple that has properties that are bound to columns.
const propertyType = property.type;
const { action: objectParser, lastColumn: lastChildColumn } = this.bindObject(propertyType, index, false);
return {
action: (src, dest) => {
const destObject = new propertyType;
objectParser(src, destObject);
dest[key] = destObject;
},
lastColumn: lastChildColumn
};
}
}
private bindSingleColumn(index: number, property: ColumnProperty, type: new () => any,
key: PropertyKey | number): ParseTupleAction {
if (this.boundColumns[index]) {
throw new Error(`Column '${this.schema.name}[${index}]' is bound to multiple columns in root type '${this.rowType.toString()}'.`);
}
const column = this.schema.columns[index];
if (column.type.type !== property.type) {
throw new Error(`Column '${this.schema.name}[${index}]' has type '${column.type.type}', but property '${type.toString()}.${property.key.toString()}' expected type '${property.type}'.`);
}
this.boundColumns[index] = true;
if (isElement(property) && (column.type.type === 'e')) {
const hasLabel = column.type.hasLabel;
this.checkElementProperty(index, 'label', hasLabel, property.options.label);
const hasLocation = column.type.locationStyle !== LocationStyle.None;
this.checkElementProperty(index, 'location', hasLocation, property.options.location);
return (src, dest) => {
const srcElement = <ElementBase>src[index];
const destElement: ElementBase = {
id: srcElement.id
};
if (hasLabel) {
destElement.label = srcElement.label;
}
if (hasLocation) {
destElement.location = srcElement.location;
}
dest[key] = destElement;
};
}
else {
return (src, dest) => {
dest[key] = src[index];
};
}
}
}
type ArrayElementType<T> = T extends Array<infer U> ? U : never;
export type CustomResultSets<T> = {
[P in keyof T]: CustomResultSet<ArrayElementType<T[P]>>;
}
export function createCustomResultSets<T>(reader: ResultSetsReader, type: { new(): T }):
CustomResultSets<T> {
const tableProperties: TableProperty[] | undefined = Reflect.getMetadata(tablePropertiesSymbol, type.prototype);
if (tableProperties === undefined) {
throw new Error(`Type '${type.toString()}' does not have any properties decorated with '@table'.`);
}
const customResultSets: Partial<CustomResultSets<T>> = {};
const boundProperties = new Set<PropertyKey>();
for (const resultSet of reader.resultSets) {
const tableProperty = findPropertyForTable(resultSet.schema, tableProperties);
if (tableProperty === undefined) {
throw new Error(`No matching property found for result set '${resultSet.schema.name}'.`);
}
if (boundProperties.has(tableProperty.key)) {
throw new Error(`Multiple result sets bound to property '${tableProperty.key.toString()}'.`);
}
boundProperties.add(tableProperty.key);
customResultSets[tableProperty.key] = CustomResultSetBinder.bind(resultSet,
tableProperty.rowType);
}
for (const tableProperty of tableProperties) {
if (!boundProperties.has(tableProperty.key)) {
throw new Error(`No matching table found for property '${tableProperty.key.toString()}'.`);
}
}
return <CustomResultSets<T>>customResultSets;
}
function findPropertyForTable(resultSet: ResultSetSchema, tableProperties: TableProperty[]):
TableProperty | undefined {
const tableName = resultSet.name === '#select' ? 'select' : resultSet.name;
return tableProperties.find(tableProperty => tableProperty.tableNames.find(name => name === tableName));
}

View File

@@ -0,0 +1,191 @@
import { RandomAccessReader, StreamDigester } from 'semmle-io';
import { parseResultSetsHeader, StringPool, parseResultSetSchema, readTuples } from './bqrs-parse';
import { ResultSetsSchema, ResultSetSchema } from './bqrs-schema';
import { ColumnValue } from './bqrs-results';
/**
* The result of parsing data from a specific file region.
*/
interface RegionResult<T> {
/** The parsed data. */
result: T,
/** The exclusive end position of the parsed data in the file. */
finalOffset: number
}
/** Reads data from the specified region of the file, and parses it using the given function. */
async function inFileRegion<T>(
file: RandomAccessReader,
start: number,
end: number | undefined,
parse: (d: StreamDigester) => Promise<T>
): Promise<RegionResult<T>> {
const stream = file.readStream(start, end);
try {
const d = StreamDigester.fromChunkIterator(stream);
const result = await parse(d);
return {
result: result,
finalOffset: start + d.position
};
}
finally {
stream.dispose();
}
}
/**
* A single result set in a BQRS file.
*/
export interface ResultSetReader {
/**
* The schema that describes the result set.
*/
readonly schema: ResultSetSchema;
/**
* Reads all of the tuples in the result set.
*/
readTuples(): AsyncIterableIterator<ColumnValue[]>;
}
/**
* A Binary Query Result Sets ("BQRS") file.
*
* @remarks
* Allows independant access to individual tables without having to parse the entire file up front.
*/
export interface ResultSetsReader {
readonly schema: ResultSetsSchema;
readonly resultSets: readonly ResultSetReader[];
findResultSetByName(name: string): ResultSetReader | undefined;
}
/**
* Metadata for a single `ResultSet` in a BQRS file.
* Does not contain the result tuples themselves.
* Includes the offset and length of the tuple data in the file,
* which can be used to read the tuples.
*/
interface ResultSetInfo {
schema: ResultSetSchema;
rowsOffset: number;
rowsLength: number;
}
class ResultSetReaderImpl implements ResultSetReader {
public readonly schema: ResultSetSchema;
private readonly rowsOffset: number;
private readonly rowsLength: number;
public constructor(private readonly resultSets: ResultSetsReaderImpl, info: ResultSetInfo) {
this.schema = info.schema;
this.rowsOffset = info.rowsOffset;
this.rowsLength = info.rowsLength;
}
public async* readTuples(): AsyncIterableIterator<ColumnValue[]> {
const stream = this.resultSets.file.readStream(this.rowsOffset,
this.rowsOffset + this.rowsLength);
try {
const d = StreamDigester.fromChunkIterator(stream);
for await (const tuple of readTuples(d, this.schema, await this.resultSets.getStringPool())) {
yield tuple;
}
}
finally {
stream.dispose();
}
}
}
class ResultSetsReaderImpl implements ResultSetsReader {
private stringPool?: StringPool = undefined;
private readonly _resultSets: ResultSetReaderImpl[];
private constructor(public readonly file: RandomAccessReader,
public readonly schema: ResultSetsSchema, resultSets: ResultSetInfo[],
private readonly stringPoolOffset: number) {
this._resultSets = resultSets.map((info) => {
return new ResultSetReaderImpl(this, info);
});
}
public get resultSets(): readonly ResultSetReader[] {
return this._resultSets;
}
public findResultSetByName(name: string): ResultSetReader | undefined {
return this._resultSets.find((resultSet) => resultSet.schema.name === name);
}
public async getStringPool(): Promise<StringPool> {
if (this.stringPool === undefined) {
const { result: stringPoolBuffer } = await inFileRegion(this.file, this.stringPoolOffset,
this.stringPoolOffset + this.schema.stringPoolSize,
async d => await d.read(this.schema.stringPoolSize));
this.stringPool = new StringPool(stringPoolBuffer);
}
return this.stringPool;
}
public static async open(file: RandomAccessReader): Promise<ResultSetsReader> {
// Parse the header of the entire BQRS file.
const { result: header, finalOffset: stringPoolOffset } =
await inFileRegion(file, 0, undefined, d => parseResultSetsHeader(d));
// The header is followed by a shared string pool.
// We have saved the offset and length of the string pool within the file,
// so we can read it later when needed.
// For now, skip over the string pool to reach the starting point of the first result set.
let currentResultSetOffset = stringPoolOffset + header.stringPoolSize;
// Parse information about each result set within the file.
const resultSets: ResultSetInfo[] = [];
for (let resultSetIndex = 0; resultSetIndex < header.resultSetCount; resultSetIndex++) {
// Read the length of this result set (encoded as a single byte).
// Note: reading length and schema together from a file region may be more efficient.
// Reading them separately just makes it easier to compute the
// starting offset and length of the schema.
const { result: resultSetLength, finalOffset: resultSetSchemaOffset } =
await inFileRegion(file, currentResultSetOffset, undefined, d => d.readLEB128UInt32());
// Read the schema of this result set.
const { result: resultSetSchema, finalOffset: resultSetRowsOffset } =
await inFileRegion(file, resultSetSchemaOffset, undefined, d => parseResultSetSchema(d));
const resultSetSchemaLength = resultSetRowsOffset - resultSetSchemaOffset;
// The schema is followed by the tuple/row data for the result set.
// We save the offset and length of the tuple data within the file,
// so we can read it later when needed.
const info: ResultSetInfo = {
// length of result set = length of schema + length of tuple data
// The 1 byte that encodes the length itself is not counted.
rowsLength: resultSetLength - resultSetSchemaLength,
rowsOffset: resultSetRowsOffset,
schema: resultSetSchema,
};
resultSets.push(info);
// Skip over the tuple data of the current result set,
// to reach the starting offset of the next result set.
currentResultSetOffset = info.rowsOffset + info.rowsLength;
}
const schema: ResultSetsSchema = {
version: header.version,
stringPoolSize: header.stringPoolSize,
resultSets: resultSets.map(resultSet => resultSet.schema)
};
const reader = new ResultSetsReaderImpl(file, schema, resultSets, stringPoolOffset);
return reader;
}
}
export function open(file: RandomAccessReader): Promise<ResultSetsReader> {
return ResultSetsReaderImpl.open(file);
}

View File

@@ -0,0 +1,202 @@
import { decodeUInt32 } from 'leb';
import { StreamDigester } from 'semmle-io';
import { ColumnValue, RawLocationValue } from './bqrs-results';
import { ColumnSchema, ColumnType, LocationStyle, PrimitiveTypeKind, ResultSetSchema } from './bqrs-schema';
/**
* bqrs-parse.ts
* -------
*
* Parsing Binary Query Result Set files.
* See [[https://git.semmle.com/Semmle/code/tree/master/queryserver-client/src/com/semmle/api/result/BinaryQueryResultSets.java]].
*/
const RESULT_SET_VERSION = 1;
const RESULT_SETS_VERSION = 2;
export type TupleParser = (tuple: readonly ColumnValue[]) => void;
export interface ResultSetsHeader {
version: number,
resultSetCount: number,
stringPoolSize: number
}
async function parseResultColumnType(d: StreamDigester): Promise<ColumnType> {
const t = await d.readASCIIChar();
if (t === 'e') {
const primitiveType: PrimitiveTypeKind =
(await d.readASCIIChar()) as PrimitiveTypeKind;
const hasLabel = (await d.readByte()) !== 0;
const locationStyle = await d.readByte();
return { type: 'e', locationStyle, hasLabel, primitiveType };
}
else {
return { type: <PrimitiveTypeKind>t };
}
}
async function parseColumnSchema(d: StreamDigester): Promise<ColumnSchema[]> {
const numColumns = await d.readLEB128UInt32();
const rv: ColumnSchema[] = [];
for (let i = 0; i < numColumns; i++) {
const name = await readLengthPrefixedString(d);
const type = await parseResultColumnType(d);
rv.push({ name, type });
}
return rv;
}
function getTrueStringLength(encodedLength: number): number {
const stringLength = (encodedLength as number) - 1;
if (stringLength === -1) {
// XXX why is this a possibility? Does a '(-1)-length' string
// (i.e. a single 0x00 byte) mean something different from a
// 0-length string? (i.e. a single 0x01 byte)
return 0;
}
else {
return stringLength;
}
}
export class StringPool {
public constructor(private readonly buffer: Buffer) {
}
public getString(offset: number): string {
//TODO: Memoize?
const { value: encodedStringLength, nextIndex } = decodeUInt32(this.buffer, offset);
const stringLength = getTrueStringLength(encodedStringLength);
const value = this.buffer.toString('utf8', nextIndex, nextIndex + stringLength);
return value;
}
}
export async function parseResultSetsHeader(d: StreamDigester): Promise<ResultSetsHeader> {
const version = await d.readLEB128UInt32();
if (version !== RESULT_SETS_VERSION) {
throw new Error(`Mismatched binary query results version. Got '${version}', but expected '${RESULT_SETS_VERSION}'.`);
}
const resultSetCount = await d.readLEB128UInt32();
const stringPoolSize = await d.readLEB128UInt32();
return {
version: version,
stringPoolSize: stringPoolSize,
resultSetCount: resultSetCount
};
}
async function readLengthPrefixedString(d: StreamDigester): Promise<string> {
const encodedLength = await d.readLEB128UInt32();
const stringLength = getTrueStringLength(encodedLength);
return await d.readUTF8String(stringLength);
}
export async function parseResultSetSchema(d: StreamDigester): Promise<ResultSetSchema> {
const version = await d.readLEB128UInt32();
if (version !== RESULT_SET_VERSION) {
throw new Error(`Mismatched binary query result version. Got '${version}', but expected '${RESULT_SET_VERSION}'.`);
}
const name = await readLengthPrefixedString(d);
const tupleCount = await d.readLEB128UInt32();
const columns = await parseColumnSchema(d);
return {
version: version,
name: name,
tupleCount: tupleCount,
columns: columns
};
}
async function parseString(d: StreamDigester, pool: StringPool): Promise<string> {
const stringOffset = await d.readLEB128UInt32();
const value = pool.getString(stringOffset);
return value;
}
async function parseLocation(d: StreamDigester, t: LocationStyle, pool: StringPool):
Promise<RawLocationValue | undefined> {
switch (t) {
case LocationStyle.None: return undefined;
case LocationStyle.String: return { t, loc: await parseString(d, pool) };
case LocationStyle.FivePart: {
const file = await parseString(d, pool);
const lineStart = await d.readLEB128UInt32();
const colStart = await d.readLEB128UInt32();
const lineEnd = await d.readLEB128UInt32();
const colEnd = await d.readLEB128UInt32();
return { t, file, lineStart, colStart, lineEnd, colEnd };
}
case LocationStyle.WholeFile:
throw new Error('Whole-file locations should appear as string locations in BQRS files.');
}
throw new Error(`Unknown Location Style ${t}`);
}
async function parsePrimitiveColumn(d: StreamDigester, type: PrimitiveTypeKind,
pool: StringPool): Promise<ColumnValue> {
switch (type) {
case 's': return await parseString(d, pool);
case 'b': return await d.readByte() !== 0;
case 'i': return await d.readLEB128UInt32();
case 'f': return await d.readDoubleLE();
case 'd': return await d.readDate();
case 'u': return await parseString(d, pool);
default: throw new Error(`Unknown primitive column type '${type}'.`);
}
}
export async function parseColumn(d: StreamDigester, t: ColumnType, pool: StringPool):
Promise<ColumnValue> {
if (t.type === 'e') {
let primitive = await parsePrimitiveColumn(d, t.primitiveType, pool);
const label = t.hasLabel ? await parseString(d, pool) : undefined;
const loc = await parseLocation(d, t.locationStyle, pool);
return {
id: <number | string>primitive,
label: label,
location: loc
};
}
else {
return parsePrimitiveColumn(d, t.type, pool);
}
}
export async function* readTuples(d: StreamDigester, schema: ResultSetSchema,
stringPool: StringPool): AsyncIterableIterator<ColumnValue[]> {
const { tupleCount, columns } = schema;
for (let rowIndex = 0; rowIndex < tupleCount; rowIndex++) {
const tuple: ColumnValue[] = Array(columns.length);
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
tuple[columnIndex] = await parseColumn(d, columns[columnIndex].type, stringPool);
}
yield tuple;
}
}
export async function parseTuples(d: StreamDigester, schema: ResultSetSchema,
stringPool: StringPool, tupleParser: TupleParser): Promise<void> {
const { tupleCount, columns } = schema;
// Create a single temporary tuple to hold the values we read from each row. Fill it with
// zero values initially so that we don't have to type it as `TupleValue | undefined`.
const tempTuple: ColumnValue[] = Array(columns.length).fill(0);
for (let rowIndex = 0; rowIndex < tupleCount; rowIndex++) {
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
tempTuple[columnIndex] = await parseColumn(d, columns[columnIndex].type, stringPool);
}
tupleParser(tempTuple);
}
}

View File

@@ -0,0 +1,100 @@
import { LocationStyle } from './bqrs-schema';
// See https://help.semmle.com/QL/learn-ql/ql/locations.html for how these are used.
export interface FivePartLocation {
t: LocationStyle.FivePart;
file: string;
lineStart: number;
colStart: number;
lineEnd: number;
colEnd: number;
}
export interface StringLocation {
t: LocationStyle.String;
loc: string;
}
/**
* A location representing an entire filesystem resource.
* This is usually derived from a `StringLocation` with the entire filesystem URL.
*/
export interface WholeFileLocation {
t: LocationStyle.WholeFile;
file: string;
}
export type RawLocationValue = FivePartLocation | StringLocation;
export type LocationValue = RawLocationValue | WholeFileLocation;
/** A location that may be resolved to a source code element. */
export type ResolvableLocationValue = FivePartLocation | WholeFileLocation;
/**
* The CodeQL filesystem libraries use this pattern in `getURL()` predicates
* to describe the location of an entire filesystem resource.
* Such locations appear as `StringLocation`s instead of `FivePartLocation`s.
*
* Folder resources also get similar URLs, but with the `folder` scheme.
* They are deliberately ignored here, since there is no suitable location to show the user.
*/
const WHOLE_FILE_LOCATION_REGEX = /file:\/\/(.+):0:0:0:0/;
/**
* Gets a resolvable source file location for the specified `LocationValue`, if possible.
* @param loc The location to test.
*/
export function tryGetResolvableLocation(loc: LocationValue | undefined): ResolvableLocationValue | undefined {
if (loc === undefined) {
return undefined;
}
else if ((loc.t === LocationStyle.FivePart) && loc.file) {
return loc;
}
else if ((loc.t === LocationStyle.WholeFile) && loc.file) {
return loc;
}
else if ((loc.t === LocationStyle.String) && loc.loc) {
return tryGetWholeFileLocation(loc);
}
else {
return undefined;
}
}
export function tryGetWholeFileLocation(loc: StringLocation): WholeFileLocation | undefined {
const matches = WHOLE_FILE_LOCATION_REGEX.exec(loc.loc);
if (matches && matches.length > 1 && matches[1]) {
// Whole-file location.
// We could represent this as a FivePartLocation with all numeric fields set to zero,
// but that would be a deliberate misuse as those fields are intended to be 1-based.
return {
t: LocationStyle.WholeFile,
file: matches[1]
};
} else {
return undefined;
}
}
export interface ElementBase {
id: PrimitiveColumnValue;
label?: string;
location?: LocationValue;
}
export interface ElementWithLabel extends ElementBase {
label: string;
}
export interface ElementWithLocation extends ElementBase {
location: LocationValue;
}
export interface Element extends Required<ElementBase> {
}
export type PrimitiveColumnValue = string | boolean | number | Date;
export type ColumnValue = PrimitiveColumnValue | ElementBase;

View File

@@ -0,0 +1,66 @@
export enum LocationStyle {
None = 0,
String,
FivePart,
/** Does not occur in BQRS files. Used only to distinguish whole-file locations in client code. */
WholeFile
}
/**
* A primitive type (any type other than an element).
*/
export type PrimitiveTypeKind = 's' | 'b' | 'i' | 'f' | 'd' | 'u';
/**
* A kind of type that a column may have.
*/
export type ColumnTypeKind = PrimitiveTypeKind | 'e';
/**
* A column type that is a primitive type.
*/
export interface PrimitiveColumnType {
type: PrimitiveTypeKind;
}
/**
* A column type that is an element type.
*/
export interface ElementColumnType {
type: 'e';
primitiveType: PrimitiveTypeKind;
locationStyle: LocationStyle;
hasLabel: boolean;
}
/**
* The type of a column.
*/
export type ColumnType = PrimitiveColumnType | ElementColumnType;
/**
* The schema describing a single column in a `ResultSet`.
*/
export interface ColumnSchema {
readonly name: string;
readonly type: ColumnType;
}
/**
* The schema of a single `ResultSet` in a BQRS file.
*/
export interface ResultSetSchema {
readonly version: number;
readonly name: string;
readonly tupleCount: number;
readonly columns: readonly ColumnSchema[];
}
/**
* The schema describing the contents of a BQRS file.
*/
export interface ResultSetsSchema {
readonly version: number,
readonly stringPoolSize: number,
readonly resultSets: readonly ResultSetSchema[]
}

View File

@@ -0,0 +1,18 @@
import { ResultSetSchema } from './bqrs-schema';
import { StreamDigester, ChunkIterator } from 'semmle-io';
import { parseResultSetsHeader, StringPool, parseResultSetSchema, parseTuples, TupleParser } from './bqrs-parse';
export async function parse(rs: ChunkIterator,
resultSetHandler: (resultSet: ResultSetSchema) => TupleParser): Promise<void> {
const d = StreamDigester.fromChunkIterator(rs);
const header = await parseResultSetsHeader(d);
const stringPool = new StringPool(await d.read(header.stringPoolSize));
for (let resultSetIndex = 0; resultSetIndex < header.resultSetCount; resultSetIndex++) {
await d.readLEB128UInt32(); // Length of result set. Unused.
const resultSetSchema = await parseResultSetSchema(d);
const tupleParser = resultSetHandler(resultSetSchema);
await parseTuples(d, resultSetSchema, stringPool, tupleParser);
}
}

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