├── .prettierignore ├── eval.gif ├── logo.png ├── .gitignore ├── src ├── binaries │ ├── index.ts │ ├── types.ts │ ├── resolver.ts │ ├── configs.ts │ └── installer.ts ├── util.ts ├── da │ └── activate.ts ├── opa.ts ├── ls │ └── clients │ │ └── regal.ts └── extension.ts ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── .vscodeignore ├── .prettierrc ├── dprint.json ├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ └── release.yaml ├── cspell.config.yaml ├── language-configuration.json ├── Makefile ├── tsconfig.json ├── syntaxes ├── markdown-inject.json └── Rego.tmLanguage ├── eslint.config.mjs ├── .markdownlint.yaml ├── README.md ├── package.json ├── CHANGELOG.md └── LICENSE /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | build.vsix 4 | *.log 5 | -------------------------------------------------------------------------------- /eval.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-policy-agent/vscode-opa/HEAD/eval.gif -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-policy-agent/vscode-opa/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | .DS_Store 6 | 7 | eslint-results.sarif 8 | -------------------------------------------------------------------------------- /src/binaries/index.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export * from "./configs"; 4 | export * from "./installer"; 5 | export * from "./resolver"; 6 | export * from "./types"; 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "editorconfig.editorconfig" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | tslint.json 10 | .envrc 11 | .direnv 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "printWidth": 80, 8 | "bracketSpacing": true, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludes": [], 3 | "plugins": [ 4 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.4.0.wasm", 5 | "https://plugins.dprint.dev/json-0.19.3.wasm", 6 | "https://plugins.dprint.dev/typescript-0.91.6.wasm", 7 | "https://plugins.dprint.dev/g-plane/malva-v0.7.1.wasm" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "npm" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | groups: 12 | dependencies: 13 | patterns: 14 | - "*" 15 | -------------------------------------------------------------------------------- /cspell.config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | language: en 3 | 4 | ignorePaths: 5 | 6 | languageSettings: 7 | - languageId: "markdown" 8 | ignoreRegExpList: 9 | # ignore inside code blocks 10 | - "/^\\s*```[\\s\\S]*?^\\s*```/gm" 11 | # ignore in code spans 12 | - "/`(.*)`/g" 13 | 14 | words: 15 | - rego # OPA language 16 | - sarif # test output format 17 | - Styra 18 | 19 | ignoreWords: 20 | - tjons # OPA contributor GH handle 21 | - dprint 22 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "#" 4 | }, 5 | "brackets": [ 6 | ["{", "}"], 7 | ["[", "]"], 8 | ["(", ")"] 9 | ], 10 | "autoClosingPairs": [ 11 | ["{", "}"], 12 | ["[", "]"], 13 | ["(", ")"], 14 | ["\"", "\""], 15 | ["`", "`"] 16 | ], 17 | "surroundingPairs": [ 18 | ["{", "}"], 19 | ["[", "]"], 20 | ["(", ")"], 21 | ["\"", "\""], 22 | ["`", "`"] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: fmt 2 | fmt: 3 | npx eslint . --fix 4 | npx dprint fmt 5 | 6 | .PHONY: lint 7 | lint: 8 | npx dprint check 9 | npx eslint --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif . 10 | npx tsc --noEmit 11 | npx cspell lint -c cspell.config.yaml '**/*.md' 12 | npx markdownlint-cli2 'README.md' 'CHANGELOG.md' '#node_modules' --config=.markdownlint.yaml 13 | 14 | .PHONY: build 15 | build: 16 | npx tsc -p ./ 17 | 18 | .PHONY: clean 19 | clean: 20 | rm -rf out/ 21 | rm -f build.vsix 22 | -------------------------------------------------------------------------------- /src/binaries/types.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export interface Logger { 4 | appendLine(message: string): void; 5 | show?(preserveFocus?: boolean): void; 6 | } 7 | 8 | export interface BinaryConfig { 9 | name: string; 10 | configKey: string; 11 | repo: string; 12 | minimumVersion?: string; 13 | assetFilter: (assets: any[], os: string, arch: string) => any; 14 | versionParser: (executablePath: string) => { version: string; error?: string }; 15 | } 16 | 17 | export interface BinaryInfo { 18 | path?: string; 19 | source: "configured" | "system" | "missing"; 20 | originalPath?: string; 21 | version: string; 22 | error?: string; 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": [ 6 | "es2020" 7 | ], 8 | "outDir": "out", 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "skipLibCheck": true, 12 | 13 | "strict": true, 14 | 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "allowUnreachableCode": false, 20 | "noUncheckedIndexedAccess": true, 21 | "exactOptionalPropertyTypes": true 22 | }, 23 | "exclude": [ 24 | "node_modules", 25 | ".vscode-test" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Auto-formatting and linting settings 10 | "editor.formatOnSave": true, 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "explicit" 14 | }, 15 | // TypeScript/JavaScript specific settings 16 | "[typescript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[javascript]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[json]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[jsonc]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v6 16 | 17 | - name: Install dependencies 18 | run: | 19 | npm install --include=dev 20 | 21 | - name: Test build package 22 | run: npx vsce package -o build.vsix 23 | 24 | lint: 25 | runs-on: ubuntu-latest 26 | permissions: 27 | contents: read 28 | security-events: write 29 | 30 | steps: 31 | - uses: actions/checkout@v6 32 | 33 | - name: Set up Node.js 34 | uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 35 | with: 36 | node-version: "22.6.0" 37 | 38 | - name: Install dependencies 39 | run: npm install 40 | 41 | - name: Run lint (dprint + eslint + typecheck + cspell + markdownlint) 42 | run: make lint 43 | continue-on-error: true 44 | 45 | - name: Upload analysis results to GitHub 46 | uses: github/codeql-action/upload-sarif@v4 47 | with: 48 | sarif_file: eslint-results.sarif 49 | -------------------------------------------------------------------------------- /syntaxes/markdown-inject.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:text.html.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#rego-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "rego-code-block": { 11 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(rego)(\\s+[^`~]*)?$)", 12 | "name": "markup.fenced_code.block.markdown", 13 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 14 | "beginCaptures": { 15 | "3": { 16 | "name": "punctuation.definition.markdown" 17 | }, 18 | "4": { 19 | "name": "fenced_code.block.language.markdown" 20 | }, 21 | "5": { 22 | "name": "fenced_code.block.language.attributes.markdown" 23 | } 24 | }, 25 | "endCaptures": { 26 | "3": { 27 | "name": "punctuation.definition.markdown" 28 | } 29 | }, 30 | "patterns": [ 31 | { 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.rego", 35 | "patterns": [ 36 | { 37 | "include": "source.rego" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | }, 44 | "scopeName": "markdown.rego.codeblock" 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "autoAttachChildProcesses": true, 16 | "preLaunchTask": { 17 | "type": "npm", 18 | "script": "watch" 19 | }, 20 | "env": { 21 | "VSCODE_DEBUG_MODE": "true" 22 | } 23 | }, 24 | { 25 | "name": "Extension Tests", 26 | "type": "extensionHost", 27 | "request": "launch", 28 | "runtimeExecutable": "${execPath}", 29 | "args": [ 30 | "--extensionDevelopmentPath=${workspaceFolder}", 31 | "--extensionTestsPath=${workspaceFolder}/out/test" 32 | ], 33 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 34 | "preLaunchTask": "npm: watch" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v6 14 | 15 | - name: Check that package.json is up-to-date 16 | run: | 17 | # this version has no v prefix, e.g. 1.0.0 18 | VERSION_FROM_PACKAGE_JSON=$(jq -r '.version' package.json) 19 | # the GITHUB_REF_NAME is the tag name, e.g. v1.0.0 20 | VERSION_FROM_GITHUB_REF_NAME=${GITHUB_REF_NAME:1} # remove the v prefix 21 | 22 | if [ "$VERSION_FROM_PACKAGE_JSON" = "$VERSION_FROM_GITHUB_REF_NAME" ]; then 23 | echo "The versions match, proceeding to publish." 24 | else 25 | echo "Please update the version in package.json to match the tag." 26 | exit 1 27 | fi 28 | 29 | - name: Set up Node.js 30 | uses: actions/setup-node@v6 31 | with: 32 | node-version: "14" 33 | 34 | - name: Install dependencies 35 | run: npm install 36 | 37 | - name: Install vsce 38 | run: npm install -g vsce 39 | 40 | - name: Publish to marketplace 41 | run: | 42 | vsce publish \ 43 | --no-update-package-json \ 44 | --no-git-tag-version \ 45 | -p ${{ secrets.VSCODE_MARKETPLACE_TOKEN }} \ 46 | ${GITHUB_REF_NAME:1} 47 | -------------------------------------------------------------------------------- /src/binaries/resolver.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { sync as commandExistsSync } from "command-exists"; 4 | import { existsSync } from "fs"; 5 | import * as vscode from "vscode"; 6 | import { replaceWorkspaceFolderPathVariable } from "../util"; 7 | import { BinaryConfig, BinaryInfo } from "./types"; 8 | 9 | export function resolveBinary(config: BinaryConfig, fallbackPath?: string): BinaryInfo { 10 | let path = vscode.workspace.getConfiguration("opa.dependency_paths").get(config.configKey); 11 | 12 | if (path?.trim()) { 13 | const originalPath = path; 14 | path = replaceWorkspaceFolderPathVariable(path); 15 | 16 | if (path.startsWith("file://")) { 17 | path = path.substring(7); 18 | } 19 | 20 | if (existsSync(path)) { 21 | const versionInfo = config.versionParser(path); 22 | return { 23 | path, 24 | source: "configured", 25 | originalPath, 26 | version: versionInfo.version, 27 | ...(versionInfo.error && { error: versionInfo.error }), 28 | }; 29 | } 30 | } 31 | 32 | const systemPath = fallbackPath || config.configKey; 33 | if (commandExistsSync(systemPath)) { 34 | const versionInfo = config.versionParser(systemPath); 35 | return { 36 | path: systemPath, 37 | source: "system", 38 | version: versionInfo.version, 39 | ...(versionInfo.error && { error: versionInfo.error }), 40 | }; 41 | } 42 | 43 | return { 44 | source: "missing", 45 | version: "missing", 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import pluginJs from "@eslint/js"; 2 | import stylistic from "@stylistic/eslint-plugin"; 3 | import globals from "globals"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default tseslint.config( 7 | { 8 | ignores: [ 9 | "node_modules/", 10 | "out/", 11 | ], 12 | }, 13 | { 14 | files: ["**/*.{js,mjs,jsx}"], 15 | languageOptions: { 16 | ecmaVersion: 2022, 17 | sourceType: "module", 18 | globals: { 19 | ...globals.browser, 20 | ...globals.node, 21 | }, 22 | }, 23 | ...pluginJs.configs.recommended, 24 | }, 25 | { 26 | files: ["**/*.{ts,tsx}"], 27 | languageOptions: { 28 | ecmaVersion: 2022, 29 | sourceType: "module", 30 | globals: { 31 | ...globals.browser, 32 | ...globals.node, 33 | }, 34 | parser: tseslint.parser, 35 | parserOptions: { 36 | project: "./tsconfig.json", 37 | }, 38 | }, 39 | plugins: { 40 | "@typescript-eslint": tseslint.plugin, 41 | "@stylistic": stylistic, 42 | }, 43 | rules: { 44 | ...tseslint.configs.eslintRecommended.rules, 45 | ...tseslint.configs.strict.rules, 46 | 47 | // Keep useful stylistic rules that don't conflict with Prettier 48 | "@stylistic/no-trailing-spaces": "error", 49 | "@stylistic/no-multiple-empty-lines": "error", 50 | 51 | // Disabled because Prettier handles these 52 | "@stylistic/no-extra-semi": "off", 53 | "@stylistic/quotes": "off", 54 | "@stylistic/semi": "off", 55 | "@stylistic/brace-style": "off", 56 | }, 57 | }, 58 | ); 59 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as vscode from "vscode"; 4 | 5 | /** 6 | * String helpers for OPA types. 7 | */ 8 | export function getPackage(parsed: any): string { 9 | return getPathString(parsed["package"].path.slice(1)); 10 | } 11 | 12 | export function getImports(parsed: any): string[] { 13 | if (parsed.imports !== undefined) { 14 | return parsed.imports.map((x: any) => { 15 | const str = getPathString(x.path.value); 16 | if (!x.alias) { 17 | return str; 18 | } 19 | return str + " as " + x.alias; 20 | }); 21 | } 22 | return []; 23 | } 24 | 25 | export function getPathString(path: any): string { 26 | let i = -1; 27 | return path.map((x: any) => { 28 | i++; 29 | if (i === 0) { 30 | return x.value; 31 | } else { 32 | if (x.value.match("^[a-zA-Z_][a-zA-Z_0-9]*$")) { 33 | return "." + x.value; 34 | } 35 | return "[\"" + x.value + "\"]"; 36 | } 37 | }).join(""); 38 | } 39 | 40 | export function getPrettyTime(ns: number): string { 41 | const seconds = ns / 1e9; 42 | if (seconds >= 1) { 43 | return seconds.toString() + "s"; 44 | } 45 | const milliseconds = ns / 1e6; 46 | if (milliseconds >= 1) { 47 | return milliseconds.toString() + "ms"; 48 | } 49 | return (ns / 1e3).toString() + "µs"; 50 | } 51 | 52 | export function replaceWorkspaceFolderPathVariable(path: string): string { 53 | if (vscode.workspace.workspaceFolders !== undefined && vscode.workspace.workspaceFolders.length > 0) { 54 | // here, uri.fsPath is used as this returns a usable path on both Windows and Unix 55 | const firstWorkspaceFolder = vscode.workspace.workspaceFolders[0]; 56 | if (firstWorkspaceFolder) { 57 | const workspaceFolderPath = firstWorkspaceFolder.uri.fsPath; 58 | path = path.replace("${workspaceFolder}", workspaceFolderPath); 59 | path = path.replace("${workspacePath}", workspaceFolderPath); 60 | } 61 | } else if (path.indexOf("${workspaceFolder}") >= 0) { 62 | vscode.window.showWarningMessage( 63 | "${workspaceFolder} variable configured in settings, but no workspace is active", 64 | ); 65 | } 66 | 67 | return path; 68 | } 69 | -------------------------------------------------------------------------------- /src/binaries/configs.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { BinaryConfig } from "./types"; 4 | 5 | const platformMap: { [key: string]: string } = { 6 | "darwin": "Darwin", 7 | "linux": "Linux", 8 | "win32": "Windows", 9 | }; 10 | 11 | export const REGAL_CONFIG: BinaryConfig = { 12 | name: "Regal", 13 | configKey: "regal", 14 | repo: "open-policy-agent/regal", 15 | minimumVersion: "0.18.0", 16 | assetFilter: (assets, os) => { 17 | const platformName = platformMap[os]; 18 | if (!platformName) { 19 | return undefined; 20 | } 21 | return assets.filter((asset: { name: string }) => asset.name.indexOf(platformName) !== -1)[0]; 22 | }, 23 | versionParser: (executablePath: string) => { 24 | try { 25 | const { execSync } = require("child_process"); 26 | const versionJSON = execSync(executablePath + " version --format=json").toString().trim(); 27 | const versionObj = JSON.parse(versionJSON); 28 | return { version: versionObj.version || "unknown" }; 29 | } catch (error) { 30 | return { version: "unknown", error: String(error) }; 31 | } 32 | }, 33 | }; 34 | 35 | export const OPA_CONFIG: BinaryConfig = { 36 | name: "OPA", 37 | configKey: "opa", 38 | repo: "open-policy-agent/opa", 39 | assetFilter: (assets, os, arch) => { 40 | const nodeArch = arch.indexOf("arm") !== -1 ? "arm64" : "amd64"; 41 | 42 | let binaryName: string; 43 | switch (os) { 44 | case "darwin": 45 | binaryName = `darwin_${nodeArch}`; 46 | break; 47 | case "linux": 48 | binaryName = `linux_${nodeArch}`; 49 | break; 50 | case "win32": 51 | if (nodeArch === "arm64") { 52 | throw "OPA binaries are not supported for windows/arm architecture. To use the features of this plugin, compile OPA from source."; 53 | } 54 | binaryName = "windows"; 55 | break; 56 | default: 57 | return { browser_download_url: "" }; 58 | } 59 | 60 | return assets.find((asset: { name: string }) => asset.name.includes(binaryName)); 61 | }, 62 | versionParser: (executablePath: string) => { 63 | try { 64 | const { spawnSync } = require("child_process"); 65 | const result = spawnSync(executablePath, ["version"]); 66 | if (result.status !== 0) { 67 | return { version: "unknown", error: "Failed to get version" }; 68 | } 69 | 70 | const firstLine = result.stdout.toString().split("\n")[0].trim(); 71 | const parts = firstLine.split(": "); 72 | if (parts.length === 2 && parts[0] === "Version") { 73 | return { version: parts[1] }; 74 | } 75 | return { version: "unknown" }; 76 | } catch (error) { 77 | return { version: "unknown", error: String(error) }; 78 | } 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/binaries/installer.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as fs from "fs"; 4 | import * as os from "os"; 5 | import { URL } from "url"; 6 | import * as vscode from "vscode"; 7 | import { BinaryConfig, Logger } from "./types"; 8 | 9 | export async function installBinary( 10 | config: BinaryConfig, 11 | logger: Logger = { appendLine: () => {}, show: () => {} }, 12 | ): Promise { 13 | if (logger.show) { 14 | logger.show(true); 15 | } 16 | 17 | logger.appendLine(`Downloading ${config.repo} executable...`); 18 | 19 | const response = await fetch(`https://api.github.com/repos/${config.repo}/releases/latest`, { 20 | headers: { 21 | "User-Agent": getUserAgent(), 22 | }, 23 | }); 24 | 25 | if (!response.ok) { 26 | throw new Error(`Failed to fetch release info: ${response.status} ${response.statusText}`); 27 | } 28 | 29 | const release = await response.json() as any; 30 | const assets = release.assets || []; 31 | const platform = process.platform; 32 | const arch = process.arch; 33 | const targetAsset = config.assetFilter(assets, platform, arch); 34 | 35 | if (!targetAsset || !targetAsset.browser_download_url) { 36 | logger.appendLine(`${config.name}: no release found for platform ${platform}`); 37 | throw new Error(`No release found for platform ${platform}`); 38 | } 39 | 40 | const downloadUrl = new URL(targetAsset.browser_download_url); 41 | const dest = os.homedir(); 42 | const path = `${dest}/${config.configKey}`; 43 | 44 | const downloadResponse = await fetch(downloadUrl.href, { 45 | headers: { 46 | "User-Agent": getUserAgent(), 47 | }, 48 | }); 49 | 50 | if (!downloadResponse.ok) { 51 | throw new Error( 52 | `Failed to download ${downloadUrl.href}: ${downloadResponse.status} ${downloadResponse.statusText}`, 53 | ); 54 | } 55 | 56 | const buffer = await downloadResponse.arrayBuffer(); 57 | fs.writeFileSync(path, Buffer.from(buffer)); 58 | 59 | logger.appendLine(`Executable downloaded to ${path}`); 60 | 61 | try { 62 | fs.chmodSync(path, 0o755); 63 | } catch (e) { 64 | logger.appendLine(e as string); 65 | throw e; 66 | } 67 | 68 | const currentConfig = vscode.workspace.getConfiguration("opa.dependency_paths").get(config.configKey); 69 | if (!currentConfig || currentConfig !== path) { 70 | logger.appendLine(`Setting 'opa.dependency_paths.${config.configKey}' to '${path}'`); 71 | } 72 | 73 | try { 74 | await vscode.workspace.getConfiguration("opa.dependency_paths").update(config.configKey, path, true); 75 | } catch (e) { 76 | logger.appendLine("Something went wrong while saving the config setting:"); 77 | logger.appendLine(e as string); 78 | throw e; 79 | } 80 | 81 | logger.appendLine(`Successfully installed ${config.repo}!`); 82 | return path; 83 | } 84 | 85 | function getUserAgent(): string { 86 | const platform = process.platform; 87 | const arch = process.arch; 88 | const nodeVersion = process.version; 89 | const vscodeVersion = vscode.version; 90 | 91 | return `vscode-opa (${platform} ${arch}; Node.js ${nodeVersion}) vscode/${vscodeVersion}`; 92 | } 93 | -------------------------------------------------------------------------------- /src/da/activate.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { REGAL_CONFIG, resolveBinary } from "../binaries"; 4 | import { existsSync, getInputPath, opaOutputChannel } from "../extension"; 5 | import * as opa from "./../opa"; 6 | 7 | const minimumSupportedRegalVersion = "0.26.0"; 8 | 9 | export function activateDebugger(context: vscode.ExtensionContext) { 10 | context.subscriptions.push( 11 | vscode.commands.registerCommand("opa.debug.debugWorkspace", (resource: vscode.Uri) => { 12 | let targetResource = resource; 13 | if (!targetResource && vscode.window.activeTextEditor) { 14 | targetResource = vscode.window.activeTextEditor.document.uri; 15 | } 16 | 17 | // Only add the inputPath if the file exists 18 | let inputPath: string | undefined = getInputPath(); 19 | if (!existsSync(inputPath)) { 20 | inputPath = undefined; 21 | } 22 | 23 | if (targetResource) { 24 | vscode.debug.startDebugging(undefined, { 25 | type: "opa-debug", 26 | name: "Debug Workspace", 27 | request: "launch", 28 | command: "eval", 29 | query: "data", 30 | inputPath: inputPath, 31 | stopOnEntry: true, 32 | enablePrint: true, 33 | }); 34 | } 35 | }), 36 | ); 37 | 38 | const provider = new OpaDebugConfigurationProvider(); 39 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("opa-debug", provider)); 40 | 41 | context.subscriptions.push( 42 | vscode.debug.registerDebugAdapterDescriptorFactory("opa-debug", new OpaDebugAdapterExecutableFactory()), 43 | ); 44 | 45 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("opa-debug", { 46 | provideDebugConfigurations( 47 | _folder: vscode.WorkspaceFolder | undefined, 48 | ): vscode.ProviderResult { 49 | return [ 50 | { 51 | name: "Launch Rego Workspace", 52 | request: "launch", 53 | type: "opa-debug", 54 | command: "eval", 55 | query: "data", 56 | enablePrint: true, 57 | }, 58 | ]; 59 | }, 60 | }, vscode.DebugConfigurationProviderTriggerKind.Dynamic)); 61 | 62 | context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("opa-debug", { 63 | provideDebugConfigurations( 64 | _folder: vscode.WorkspaceFolder | undefined, 65 | ): vscode.ProviderResult { 66 | return [ 67 | { 68 | name: "Launch Rego Workspace", 69 | request: "launch", 70 | type: "opa-debug", 71 | command: "eval", 72 | query: "data", 73 | inputPath: "${workspaceFolder}/input.json", 74 | enablePrint: true, 75 | }, 76 | ]; 77 | }, 78 | }, vscode.DebugConfigurationProviderTriggerKind.Initial)); 79 | } 80 | 81 | class OpaDebugConfigurationProvider implements vscode.DebugConfigurationProvider { 82 | /** 83 | * Massage a debug configuration just before a debug session is being launched, 84 | * e.g. add all missing attributes to the debug configuration. 85 | */ 86 | resolveDebugConfiguration( 87 | _folder: vscode.WorkspaceFolder | undefined, 88 | config: vscode.DebugConfiguration, 89 | _token?: vscode.CancellationToken, 90 | ): vscode.ProviderResult { 91 | // if launch.json is missing or empty 92 | if (!config.type && !config.request && !config.name) { 93 | const editor = vscode.window.activeTextEditor; 94 | if (editor && editor.document.languageId === "rego") { 95 | config.type = "opa-debug"; 96 | config.name = "Launch"; 97 | config.request = "launch"; 98 | config.command = "eval"; 99 | config.query = "data"; 100 | config.stopOnEntry = true; 101 | config.enablePrint = true; 102 | } 103 | } 104 | 105 | if (!config.bundlePaths) { 106 | // if bundlePaths isn't set, default to opa.roots 107 | config.bundlePaths = opa.getRoots(); 108 | } 109 | 110 | if (config.request === "attach" && !config.program) { 111 | return vscode.window.showInformationMessage("Cannot find a program to debug").then((_) => { 112 | return undefined; // abort launch 113 | }); 114 | } 115 | 116 | if (config.request === "launch" && !config.dataPaths && !config.bundlePaths) { 117 | return vscode.window.showInformationMessage("Cannot find Rego to debug").then((_) => { 118 | return undefined; // abort launch 119 | }); 120 | } 121 | 122 | return config; 123 | } 124 | } 125 | 126 | class OpaDebugAdapterExecutableFactory implements vscode.DebugAdapterDescriptorFactory { 127 | createDebugAdapterDescriptor( 128 | _session: vscode.DebugSession, 129 | executable: vscode.DebugAdapterExecutable | undefined, 130 | ): vscode.ProviderResult { 131 | if (!executable) { 132 | opaOutputChannel.appendLine(`Regal: creating debug adapter (minimum version: ${minimumSupportedRegalVersion})`); 133 | 134 | const regalBinaryInfo = resolveBinary(REGAL_CONFIG, "regal"); 135 | if (!regalBinaryInfo.path) { 136 | vscode.window.showWarningMessage("Regal binary not found. Please install Regal for debugging support."); 137 | return; 138 | } 139 | 140 | const regalExecutable = regalBinaryInfo.path; 141 | 142 | executable = new vscode.DebugAdapterExecutable(regalExecutable, ["debug"]); 143 | } 144 | return executable; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Refer to https://github.com/DavidAnson/markdownlint for additional rules 2 | 3 | # Set default state for rules to false in order to handle markdownlint new rule additions 4 | default: false 5 | 6 | # MD001/header-increment - Heading levels should only increment by one level at a time 7 | MD001: true 8 | 9 | # MD003/header-style - Heading style 10 | MD003: 11 | style: "atx" 12 | 13 | # MD004/ul-style - Unordered list style 14 | MD004: 15 | style: "dash" 16 | 17 | # MD005/list-indent - Inconsistent indentation for list items at the same level 18 | MD005: true 19 | 20 | # MD007/ul-indent - Unordered list indentation 21 | MD007: 22 | indent: 2 23 | start_indented: false 24 | 25 | # MD009/no-trailing-spaces - Trailing spaces 26 | MD009: 27 | # Spaces for line break 28 | br_spaces: 2 29 | # Disallow spaces in empty lines in lists 30 | list_item_empty_lines: false 31 | # Report unnecessary breaks 32 | strict: true 33 | 34 | # MD010/no-hard-tabs - Hard tabs 35 | MD010: 36 | code_blocks: false 37 | spaces_per_tab: 2 38 | 39 | # MD011/no-reversed-links - Reversed link syntax 40 | MD011: true 41 | 42 | # MD012/no-multiple-blanks - Multiple consecutive blank lines 43 | MD012: 44 | maximum: 2 45 | 46 | # MD013/line-length - Line length 47 | MD013: false 48 | 49 | # MD014/commands-show-output - Dollar signs used before commands without showing output 50 | MD014: true 51 | 52 | # MD018/no-missing-space-atx - No space after hash on atx style heading 53 | MD018: true 54 | 55 | # MD019/no-multiple-space-atx - Multiple spaces after hash on atx style heading 56 | MD019: true 57 | 58 | # MD022/blanks-around-headers - Headings should be surrounded by blank lines 59 | MD022: 60 | lines_above: 2 61 | lines_below: 1 62 | 63 | # MD023/header-start-left - Headings must start at the beginning of the line 64 | MD023: true 65 | 66 | # MD024/no-duplicate-header - Multiple headings with the same content 67 | MD024: 68 | # Heading duplication allowed for non-sibling headings 69 | siblings_only: true 70 | 71 | # MD025/single-title - Multiple top-level headings in the same document 72 | MD025: 73 | # h1 level 74 | level: 1 75 | # Ignore front matter title param 76 | front_matter_title: "" 77 | 78 | # MD026/no-trailing-punctuation - Trailing punctuation in heading 79 | MD026: 80 | # Punctuation characters 81 | punctuation: ".,;:!。,;:!" 82 | 83 | # MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol 84 | MD027: true 85 | 86 | # MD028/no-blanks-blockquote - Blank line inside blockquote 87 | MD028: true 88 | 89 | # MD029/ol-prefix - Ordered list item prefix 90 | MD029: 91 | # Used 1, 2, 3 style, or 1., 1., 1. style 92 | style: "one_or_ordered" 93 | 94 | # MD030/list-marker-space - Spaces after list markers 95 | MD030: 96 | # Spaces for single-line unordered list items 97 | ul_single: 1 98 | # Spaces for single-line ordered list items 99 | ol_single: 1 100 | # Spaces for multi-line unordered list items 101 | ul_multi: 1 102 | # Spaces for multi-line ordered list items 103 | ol_multi: 1 104 | 105 | # MD031/blanks-around-fences - Fenced code blocks should be surrounded by blank lines 106 | MD031: false 107 | 108 | # MD032/blanks-around-lists - Lists should be surrounded by blank lines 109 | MD032: false 110 | 111 | # MD033/no-inline-html - Inline HTML 112 | MD033: false 113 | 114 | # MD034/no-bare-urls - Bare URL used 115 | MD034: true 116 | 117 | # MD035/hr-style - Horizontal rule style 118 | MD035: 119 | style: "---" 120 | 121 | # MD036/no-emphasis-as-header - Emphasis used instead of a heading 122 | MD036: 123 | # Punctuation characters 124 | punctuation: ".,;:!?。,;:!?" 125 | 126 | # MD037/no-space-in-emphasis - Spaces inside emphasis markers 127 | MD037: true 128 | 129 | # MD038/no-space-in-code - Spaces inside code span elements 130 | MD038: true 131 | 132 | # MD039/no-space-in-links - Spaces inside link text 133 | MD039: true 134 | 135 | # MD040/fenced-code-language - Fenced code blocks should have a language specified 136 | MD040: true 137 | 138 | # MD041/first-line-h1 - First line in a file should be a top-level heading 139 | MD041: 140 | # Heading level 141 | level: 1 142 | # RegExp for matching title in front matter 143 | front_matter_title: "^\\s*title\\s*[:=]" 144 | 145 | # MD042/no-empty-links - No empty links 146 | MD042: true 147 | 148 | # MD043/required-headers - Required heading structure 149 | MD043: false 150 | 151 | # MD044/proper-names - Proper names should have the correct capitalization 152 | MD044: 153 | # List of proper names 154 | names: [ 155 | # Companies/Organizations/Products 156 | "ACR", 157 | "AKS", 158 | "Amazon", 159 | "Anthos", 160 | "Auth0", 161 | "AWS", 162 | "Azure", 163 | "Bitbucket", 164 | "CIS", 165 | "CloudFormation", 166 | "CNCF", 167 | "DAS", 168 | "DDB", 169 | "Docker", 170 | "DSS", 171 | "DynamoDB", 172 | "EKS", 173 | "Elasticsearch", 174 | "Envoy", 175 | "GCP", 176 | "GitHub", 177 | "GKE", 178 | "Gmail", 179 | "Google", 180 | "HashiCorp", 181 | "Helm", 182 | "Istio", 183 | "JavaScript", 184 | "Kafka", 185 | "KMS", 186 | "Kong", 187 | "Kubernetes", 188 | "Kuma", 189 | "Linux", 190 | "macOS", 191 | "Microsoft", 192 | "MITRE", 193 | "NGNIX", 194 | "Okta", 195 | "OPA", 196 | "OpenAPI", 197 | "OpenShift", 198 | "PCI", 199 | "Postgres", 200 | "PostgreSQL", 201 | "MySQL", 202 | "RDS", 203 | "RHEL", 204 | "S3", 205 | "Terraform", 206 | "WebAssembly", 207 | # Terms 208 | "ABAC", 209 | "API", 210 | "CI/CD", 211 | "CLI", 212 | "DevOps", 213 | "DevSecOps", 214 | "DNS", 215 | "FAQ", 216 | "GDPR", 217 | "gRPC", 218 | "GUI", 219 | "HMAC", 220 | "HTML", 221 | "HTTP", 222 | "HTTPS", 223 | "IAM", 224 | "IP", 225 | "IRSA", 226 | "JSON", 227 | "JWT", 228 | "LDAP", 229 | "MFPA", 230 | "mTLS", 231 | "PAM", 232 | "RBAC", 233 | "REPL", 234 | "SaaS", 235 | "SASL", 236 | "SCIM", 237 | "SDK", 238 | "SLA", 239 | "SLP", 240 | "SMTP", 241 | "SSH", 242 | "SSL", 243 | "SSO", 244 | "TLS", 245 | "URL", 246 | "vCPU", 247 | "VPC", 248 | "vscode-opa", 249 | "YAML", 250 | ] 251 | # Ignore code blocks 252 | code_blocks: false 253 | html_elements: false 254 | 255 | # MD045/no-alt-text - Images should have alternate text (alt text) 256 | MD045: true 257 | 258 | # MD046/code-block-style - Code block style 259 | MD046: 260 | style: "fenced" 261 | 262 | # MD047/single-trailing-newline - Files should end with a single newline character 263 | MD047: true 264 | 265 | # MD048/code-fence-style - Code fence style 266 | MD048: 267 | style: "backtick" 268 | 269 | # MD049/emphasis-style - Emphasis style should be consistent 270 | MD049: 271 | # Use underscore for emphasis to easily distinguish from strong/bold 272 | style: "underscore" 273 | 274 | # MD050/strong-style - Strong style should be consistent 275 | MD050: 276 | style: "asterisk" 277 | -------------------------------------------------------------------------------- /src/opa.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as cp from "child_process"; 4 | import * as vscode from "vscode"; 5 | import { OPA_CONFIG, resolveBinary } from "./binaries"; 6 | import { getImports, getPackage, replaceWorkspaceFolderPathVariable } from "./util"; 7 | 8 | export function getDataDir(uri: vscode.Uri): string { 9 | // NOTE(tsandall): we don't have a precise version for 3be55ed6 so 10 | // do our best and rely on the -dev tag. 11 | if (!installedOPASameOrNewerThan("0.14.0-dev")) { 12 | return uri.fsPath; 13 | } 14 | if (uri.scheme === "file") { 15 | return uri.toString(); 16 | } 17 | return decodeURIComponent(uri.toString()); 18 | } 19 | 20 | export function canUseBundleFlags(): boolean { 21 | const bundleMode = vscode.workspace.getConfiguration("opa").get("bundleMode", true); 22 | return installedOPASameOrNewerThan("0.14.0-dev") && bundleMode; 23 | } 24 | 25 | export function canUseStrictFlag(): boolean { 26 | const strictMode = vscode.workspace.getConfiguration("opa").get("strictMode", true); 27 | return strictMode && installedOPASameOrNewerThan("0.37.0"); 28 | } 29 | 30 | function dataFlag(): string { 31 | if (canUseBundleFlags()) { 32 | return "--bundle"; 33 | } 34 | return "--data"; 35 | } 36 | 37 | // returns true if installed OPA is same or newer than OPA version x. 38 | function installedOPASameOrNewerThan(x: string): boolean { 39 | const s = getOPAVersionString(); 40 | return opaVersionSameOrNewerThan(s, x); 41 | } 42 | 43 | function replacePathVariables(path: string): string { 44 | let result = replaceWorkspaceFolderPathVariable(path); 45 | if (vscode.window.activeTextEditor !== undefined) { 46 | result = result.replace( 47 | "${fileDirname}", 48 | require("path").dirname(vscode.window.activeTextEditor!.document.fileName), 49 | ); 50 | } else if (path.indexOf("${fileDirname}") >= 0) { 51 | // Report on the original path 52 | vscode.window.showWarningMessage("${fileDirname} variable configured in settings, but no document is active"); 53 | } 54 | return result; 55 | } 56 | 57 | // Returns a list of root data path URIs based on the plugin configuration. 58 | export function getRoots(): string[] { 59 | const roots = vscode.workspace.getConfiguration("opa").get("roots", []); 60 | 61 | return roots.map((root: string) => getDataDir(vscode.Uri.parse(replacePathVariables(root)))); 62 | } 63 | 64 | // Returns a list of root data parameters in an array 65 | // like ["--bundle=file:///a/b/x/", "--bundle=file:///a/b/y"] in bundle mode 66 | // or ["--data=file:///a/b/x", "--data=file://a/b/y"] otherwise. 67 | export function getRootParams(): string[] { 68 | const flag = dataFlag(); 69 | const roots = getRoots(); 70 | 71 | return roots.map((root) => `${flag}=${root}`); 72 | } 73 | 74 | // Returns a list of schema parameters in an array. 75 | // The returned array either contains a single entry; or is empty, if the 'opa.schema' setting isn't set. 76 | export function getSchemaParams(): string[] { 77 | let schemaPath = vscode.workspace.getConfiguration("opa").get("schema"); 78 | if (schemaPath === undefined || schemaPath === null) { 79 | return []; 80 | } 81 | 82 | schemaPath = replacePathVariables(schemaPath); 83 | 84 | // At this stage, we don't care if the path is valid; let the OPA command return an error. 85 | return [`--schema=${schemaPath}`]; 86 | } 87 | 88 | // returns true if OPA version a is same or newer than OPA version b. If either 89 | // version is not in the expected format (i.e., 90 | // ..[-]) this function returns true. Major, minor, 91 | // and point versions are compared numerically. Patch versions are compared 92 | // lexicographically however an empty patch version is considered newer than a 93 | // non-empty patch version. 94 | function opaVersionSameOrNewerThan(a: string, b: string): boolean { 95 | const aVersion = parseOPAVersion(a); 96 | const bVersion = parseOPAVersion(b); 97 | 98 | if (aVersion.length !== 4 || bVersion.length !== 4) { 99 | return true; 100 | } 101 | 102 | for (let i = 0; i < 3; i++) { 103 | if (aVersion[i] > bVersion[i]) { 104 | return true; 105 | } else if (bVersion[i] > aVersion[i]) { 106 | return false; 107 | } 108 | } 109 | 110 | if (aVersion[3] === "" && bVersion[3] !== "") { 111 | return true; 112 | } else if (aVersion[3] !== "" && bVersion[3] === "") { 113 | return false; 114 | } 115 | 116 | return aVersion[3] >= bVersion[3]; 117 | } 118 | 119 | // returns array of numbers and strings representing an OPA semantic version. 120 | function parseOPAVersion(s: string): any[] { 121 | const parts = s.split(".", 3); 122 | if (parts.length < 3) { 123 | return []; 124 | } 125 | 126 | const major = Number(parts[0]); 127 | const minor = Number(parts[1]); 128 | const pointParts = parts[2]?.split("-", 2); 129 | if (!pointParts || pointParts.length === 0) { 130 | return []; 131 | } 132 | const point = Number(pointParts[0]); 133 | let patch = ""; 134 | 135 | if (pointParts.length >= 2) { 136 | patch = pointParts[1] || ""; 137 | } 138 | 139 | return [major, minor, point, patch]; 140 | } 141 | 142 | // returns the installed OPA version as a string. 143 | function getOPAVersionString(): string { 144 | const opaPath = resolveBinary(OPA_CONFIG, "opa").path; 145 | if (opaPath === undefined) { 146 | return ""; 147 | } 148 | 149 | const result = cp.spawnSync(opaPath, ["version"]); 150 | if (result.status !== 0) { 151 | return ""; 152 | } 153 | 154 | const lines = result.stdout.toString().split("\n"); 155 | 156 | for (let i = 0; i < lines.length; i++) { 157 | const line = lines[i]; 158 | if (!line) continue; 159 | const parts = line.trim().split(": ", 2); 160 | if (parts.length < 2) { 161 | continue; 162 | } 163 | if (parts[0] === "Version") { 164 | return parts[1] || ""; 165 | } 166 | } 167 | return ""; 168 | } 169 | 170 | // refToString formats a ref as a string. Strings are special-cased for 171 | // dot-style lookup. Note: this function is currently only used for populating 172 | // picklists based on dependencies. As such it doesn't handle all term types 173 | // properly. 174 | export function refToString(ref: any[]): string { 175 | const regoVarPattern = new RegExp("^[a-zA-Z_][a-zA-Z0-9_]*$"); 176 | let result = ref[0].value; 177 | for (let i = 1; i < ref.length; i++) { 178 | if (ref[i].type === "string") { 179 | if (regoVarPattern.test(ref[i].value)) { 180 | result += "." + ref[i].value; 181 | continue; 182 | } 183 | } 184 | result += "[" + JSON.stringify(ref[i].value) + "]"; 185 | } 186 | return result; 187 | } 188 | 189 | /** 190 | * Helpers for executing OPA as a subprocess. 191 | */ 192 | 193 | export function parse( 194 | opaPath: string, 195 | path: string, 196 | cb: (pkg: string, imports: string[]) => void, 197 | onerror: (output: string) => void, 198 | ) { 199 | run(opaPath, ["parse", path, "--format", "json"], "", (_, result) => { 200 | const pkg = getPackage(result); 201 | const imports = getImports(result); 202 | cb(pkg, imports); 203 | }, onerror); 204 | } 205 | 206 | export function run( 207 | path: string, 208 | args: string[], 209 | stdin: string, 210 | onSuccess: (stderr: string, result: any) => void, 211 | onFailure: (msg: string) => void, 212 | ) { 213 | runWithStatus(path, args, stdin, (code: number, stderr: string, stdout: string) => { 214 | if (code === 0) { 215 | onSuccess(stderr, JSON.parse(stdout)); 216 | } else if (stdout !== "") { 217 | onFailure(stdout); 218 | } else { 219 | onFailure(stderr); 220 | } 221 | }); 222 | } 223 | 224 | function getOpaEnv(): NodeJS.ProcessEnv { 225 | const env = vscode.workspace.getConfiguration("opa").get("env", {}); 226 | 227 | return Object.fromEntries(Object.entries(env).map(([k, v]) => [k, replacePathVariables(v as string)])); 228 | } 229 | 230 | // runWithStatus executes the OPA binary at path with args and stdin. The 231 | // callback is invoked with the exit status, stderr, and stdout buffers. 232 | export function runWithStatus( 233 | path: string, 234 | args: string[], 235 | stdin: string, 236 | cb: (code: number, stderr: string, stdout: string) => void, 237 | ) { 238 | const binaryInfo = resolveBinary(OPA_CONFIG, path); 239 | 240 | if (!binaryInfo.path) { 241 | return; 242 | } 243 | 244 | console.log("spawn:", binaryInfo.path, "args:", args.toString()); 245 | 246 | const proc = cp.spawn(binaryInfo.path, args, { env: { ...process.env, ...getOpaEnv() } }); 247 | 248 | proc.stdin.write(stdin); 249 | proc.stdin.end(); 250 | let stdout = ""; 251 | let stderr = ""; 252 | 253 | proc.stdout.on("data", (data) => { 254 | stdout += data; 255 | }); 256 | 257 | proc.stderr.on("data", (data) => { 258 | stderr += data; 259 | }); 260 | 261 | proc.on("exit", (code) => { 262 | console.log("code:", code); 263 | console.log("stdout:", stdout); 264 | console.log("stderr:", stderr); 265 | cb(code || 0, stderr, stdout); 266 | }); 267 | } 268 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-opa 2 | 3 | This plugin provides a number of features to help you work with 4 | [Open Policy Agent](https://www.openpolicyagent.org) 5 | (OPA) policies in Visual Studio Code. 6 | 7 | 8 | ## Features 9 | 10 | - Evaluate Packages 11 | - Evaluate Selections 12 | - Partially Evaluate Selections 13 | - Trace Selections 14 | - Profile Selections 15 | - Run Tests in Workspace 16 | - Toggle Coverage in Workspace 17 | - Toggle Coverage of Selections 18 | 19 | Additionally, users may choose to install 20 | [Regal](https://www.openpolicyagent.org/projects/regal), 21 | which adds the following features via the 22 | [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) (LSP): 23 | 24 | - Diagnostics (linting) 25 | - Hover / tooltips (for inline docs on built-in functions) 26 | - Go to definition (ctrl/cmd + click on a reference to go to definition) 27 | - Folding ranges (expand/collapse blocks, imports, comments) 28 | - Document and workspace symbols (navigate to rules, functions, packages) 29 | - Inlay hints (show names of built-in function arguments next to their values) 30 | - Formatting (`opa fmt`, `opa fmt --rego-v1` or `regal fix`, see [Configuration](#configuration) below) 31 | - Code actions (quick fixes for linting issues) 32 | - Code completions 33 | - Code lenses (click to evaluate any package or rule in the editor, and have the result displayed directly on the same line) 34 | 35 | To learn more about each language server feature, see the Regal [language server](https://docs.styra.com/regal/language-server) documentation. 36 | 37 | ![Use of the extension to lint and eval Rego code](https://raw.githubusercontent.com/open-policy-agent/vscode-opa/main/eval.gif) 38 | 39 | Regal also adds the ability to debug Rego modules via the [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) (DAP): 40 | 41 | - Launching `eval` debug sessions 42 | - Setting and halting on breakpoints 43 | - Stepping over, into, and out of rules, functions, every-statements, and comprehensions 44 | - Inspecting the `data` and `input` documents 45 | - Inspecting local variable values 46 | - Inspecting values in the Virtual Cache (global results of rule and function evaluations) 47 | 48 | 49 | ## Requirements 50 | 51 | - This plugin requires the [Open Policy Agent](https://github.com/open-policy-agent/opa) executable (`opa`) to be installed in your $PATH. Alternatively, you can configure the `opa.dependency_paths.opa` setting to point to the executable. If you do not have OPA installed, the plugin will prompt you to install the executable the first time you evaluate a policy, run tests, etc. 52 | 53 | 54 | ## Installation 55 | 56 | To install the extension, visit the 57 | [Visual Studio Code Marketplace](https://marketplace.visualstudio.com/items?itemName=tsandall.opa) 58 | or search for "Open Policy Agent" in the 'Extensions' panel. 59 | 60 | > [!TIP] 61 | > This extension has built-in support for linting and hover tooltips 62 | > through a native integration with [Regal](https://docs.styra.com/regal). See the 63 | > ['Getting Started'](https://docs.styra.com/regal#getting-started) guide for more 64 | > information about installing Regal. 65 | 66 | 67 | ## Configuration 68 | 69 | | Field | Default | Description | 70 | | --- | --- | --- | 71 | | `opa.dependency_paths.opa` | `null` | Set path of OPA executable. If the path contains the string `${workspaceFolder}` it will be replaced with the current workspace root. E.g., if the path is set to `${workspaceFolder}/bin/opa` and the current workspace root is `/home/alice/project`, the OPA executable path will resolve to `/home/alice/project/bin/opa`. | 72 | | `opa.checkOnSave` | `false` | Enable automatic checking of .rego files on save. | 73 | | `opa.strictMode` | `false` | Enable [strict-mode](https://www.openpolicyagent.org/docs/latest/policy-language/#strict-mode) for the `OPA: Check File Syntax command`. | 74 | | `opa.roots` | `[${workspaceFolder}]` | List of paths to load as bundles for policy and data. Defaults to a single entry which is the current workspace root. The variable `${workspaceFolder}` will be resolved as the current workspace root. The variable `${fileDirname}` will be resolved as the directory of the file currently opened in the active window. | 75 | | `opa.bundleMode` | `true` | Enable treating the workspace as a bundle to avoid loading erroneous data JSON/YAML files. It is _NOT_ recommended to disable this. | 76 | | `opa.schema` | `null` | Path to the [schema](https://www.openpolicyagent.org/docs/latest/policy-language/#using-schemas-to-enhance-the-rego-type-checker) file or directory. If set to `null`, schema evaluation is disabled. As for `opa.roots`, `${workspaceFolder}` and `${fileDirname}` variables can be used in the path. | 77 | | `opa.languageServers` | `null` | An array of enabled language servers (currently `["regal"]` is supported) | 78 | | `opa.env` | `{}` | Object of environment variables passed to the process running OPA (e.g. `{"key": "value"}`) | 79 | | `opa.formatter` | `opa-fmt` | Name of the OPA formatter to use. Requires Regal. One of `opa-fmt`, `opa-fmt-rego-v1` and `regal-fix`. This value is sent as an initialization option to the language server, so the change won't take effect before the project (or VS Code) is reloaded. See the documentation for the [regal fix](https://docs.styra.com/regal/fixing) command for more information | 80 | 81 | Note that the `${workspaceFolder}` variable will expand to a full URI of the workspace, as expected by most VS Code commands. The `${workspacePath}` variable may additionally be used where only the path component (i.e. without the `file://` schema component) of the workspace URI is required. 82 | 83 | > For bundle documentation refer to [https://www.openpolicyagent.org/docs/latest/management/#bundle-file-format](https://www.openpolicyagent.org/docs/latest/management-bundles/#bundle-file-format). 84 | Note that data files _MUST_ be named either `data.json` or `data.yaml`. 85 | 86 | 87 | ### Using `opa.env` to set OPA command line flags 88 | 89 | From OPA v0.62.0 and onwards, it's possible to set any command line flag via environment variables as an alternative to arguments to the various `opa` commands. This allows using the `opa.env` object for setting any flag to the commmands executed by the extension. The format of the environment variables follows the pattern `OPA__` where `COMMAND` is the command name in uppercase (like `EVAL`) and `FLAG` is the flag name in uppercase (like `IGNORE`). For example, to set the `--capabilities` flag for the `opa check` and `opa eval` command, use the following configuration in your `.vscode/settings.json` file: 90 | 91 | ```json 92 | { 93 | "opa.env": { 94 | "OPA_CHECK_CAPABILITIES": "${workspacePath}/misc/capabilities.json", 95 | "OPA_EVAL_CAPABILITIES": "${workspacePath}/misc/capabilities.json" 96 | } 97 | } 98 | ``` 99 | 100 | 101 | ## Tips 102 | 103 | 104 | ### Set the `input` document by creating `input.json` 105 | 106 | The extension will look for a file called `input.json` in the current directory of the policy file being evaluated, or at the root of the workspace, and will use it as the `input` document when evaluating policies. If you modify this file and re-run evaluation you will see the affect of the changes. 107 | 108 | The [code lens evaluation](https://github.com/open-policy-agent/regal/blob/main/docs/language-server.md#code-lenses-evaluation) feature (with Regal installed) shows an "Evaluate" button over any package or rule declaration, which when clicked displays the result of evaluation directly on the same line as the package or rule. Providing input for this type of evaluation is done the same way, i.e. via an `input.json` file. 109 | 110 | We recommend adding `input.json` to the `.gitignore` file of your project, so that you can evaluate policy anywhere without the risk of accidentally committing the input. 111 | 112 | 113 | ### Bind keyboard shortcuts for frequently used commands 114 | 115 | Open the keyboard shortcuts file (`keybindings.json`) for VS Code (⌘ Shift p → `Preferences: Open Keyboard Shortcuts File`) and add the following JSON snippets. 116 | 117 | Bind the `OPA: Evaluate Selection` command to a keyboard shortcut (e.g., ⌘ e) to quickly evaluate visually selected blocks in the policy. 118 | 119 | ```json 120 | { 121 | "key": "cmd+e", 122 | "command": "opa.eval.selection", 123 | "when": "editorLangId == rego" 124 | } 125 | ``` 126 | 127 | Bind the `OPA: Evaluate Package` command to a keyboard shortcut (e.g., ⌘ Shift a) to quickly evaluate the entire package and see all of the decisions. 128 | 129 | ```json 130 | { 131 | "key": "shift+cmd+a", 132 | "command": "opa.eval.package", 133 | "when": "editorLangId == rego" 134 | } 135 | ``` 136 | 137 | 138 | ### Loading arbitrary JSON/YAML as data 139 | 140 | If unable to use `data.json` or `data.yaml` files with `opa.bundleMode` enabled 141 | you can disable the configuration option and _ALL_ `*.json` and `*.yaml` files 142 | will be loaded from the workspace. 143 | 144 | 145 | ### Disabling linting 146 | 147 | If you want to disable linting for a specific workspace while retaining the other features Regal provides — for example when working with code that you can't change — you can do so by providing a `.regal/config.yaml` file at the root of the workspace. To disable linting entirely, your config file could look like this: 148 | 149 | ```yaml 150 | rules: 151 | default: 152 | level: ignore 153 | ``` 154 | 155 | Regal additionally scans for a `.regal/config.yaml` file _above_ the workspace, which can be used to disable linting for all workspaces that inherit from it, or to use a common configuration file across multiple workspaces. 156 | 157 | 158 | ## Debugging OPA command evaluation 159 | 160 | In case some command isn't behaving as you expect, you can see exactly what command was executed by opening the developer tools from the **Help** menu and check the **Console** tab. 161 | 162 | 163 | ## Development 164 | 165 | If you want to hack on the extension itself, you should clone this repository, install the dependencies (`npm install --include=dev`) and use Visual Studio Code's Debugger (F5) to test your changes. 166 | 167 | 168 | ## ROADMAP 169 | 170 | - [x] highlight syntax errors in file (available when using Regal language server) 171 | - [ ] run `opa test` on package instead of entire workspace 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opa", 3 | "icon": "logo.png", 4 | "displayName": "Open Policy Agent", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/open-policy-agent/vscode-opa.git" 8 | }, 9 | "description": "Develop, test, debug, and analyze policies for the Open Policy Agent project.", 10 | "version": "0.20.0", 11 | "publisher": "tsandall", 12 | "engines": { 13 | "vscode": "^1.106.0" 14 | }, 15 | "categories": [ 16 | "Programming Languages", 17 | "Linters", 18 | "Testing", 19 | "Debuggers" 20 | ], 21 | "keywords": [ 22 | "open policy agent", 23 | "opa", 24 | "rego", 25 | "policy", 26 | "regal" 27 | ], 28 | "main": "./out/extension", 29 | "contributes": { 30 | "menus": { 31 | "editor/title/run": [ 32 | { 33 | "command": "opa.debug.debugWorkspace", 34 | "when": "resourceLangId == rego", 35 | "group": "navigation@1" 36 | } 37 | ], 38 | "commandPalette": [ 39 | { 40 | "command": "opa.debug.debugWorkspace", 41 | "when": "resourceLangId == rego" 42 | } 43 | ] 44 | }, 45 | "commands": [ 46 | { 47 | "command": "opa.check.file", 48 | "title": "OPA: Check File Syntax" 49 | }, 50 | { 51 | "command": "opa.eval.package", 52 | "title": "OPA: Evaluate Package" 53 | }, 54 | { 55 | "command": "opa.eval.selection", 56 | "title": "OPA: Evaluate Selection" 57 | }, 58 | { 59 | "command": "opa.eval.coverage", 60 | "title": "OPA: Toggle Evaluation Coverage" 61 | }, 62 | { 63 | "command": "opa.test.workspace", 64 | "title": "OPA: Test Workspace" 65 | }, 66 | { 67 | "command": "opa.test.coverage.workspace", 68 | "title": "OPA: Toggle Workspace Coverage" 69 | }, 70 | { 71 | "command": "opa.trace.selection", 72 | "title": "OPA: Trace Selection" 73 | }, 74 | { 75 | "command": "opa.profile.selection", 76 | "title": "OPA: Profile Selection" 77 | }, 78 | { 79 | "command": "opa.partial.selection", 80 | "title": "OPA: Partial Evaluation: Selection" 81 | }, 82 | { 83 | "command": "opa.prompts.clear", 84 | "title": "OPA: Clear Dismissed Prompts" 85 | }, 86 | { 87 | "command": "opa.debug.debugWorkspace", 88 | "title": "Debug Workspace", 89 | "category": "OPA Debug", 90 | "enablement": "!inDebugMode", 91 | "icon": "$(debug-alt)" 92 | }, 93 | { 94 | "command": "opa.regal.restart", 95 | "title": "OPA: Restart Regal Language Server" 96 | } 97 | ], 98 | "configuration": { 99 | "type": "object", 100 | "title": "OPA Configuration", 101 | "properties": { 102 | "opa.dependency_paths.opa": { 103 | "type": [ 104 | "string", 105 | "null" 106 | ], 107 | "default": null, 108 | "description": "Path to OPA's binary. Defaults to null." 109 | }, 110 | "opa.dependency_paths.regal": { 111 | "type": [ 112 | "string", 113 | "null" 114 | ], 115 | "default": null, 116 | "description": "Path to Regal's binary on disk. Defaults to null." 117 | }, 118 | "opa.checkOnSave": { 119 | "type": [ 120 | "boolean" 121 | ], 122 | "default": false, 123 | "description": "Run OPA check on save. Defaults to false." 124 | }, 125 | "opa.roots": { 126 | "type": [ 127 | "array" 128 | ], 129 | "default": [ 130 | "${workspaceFolder}" 131 | ], 132 | "description": "List of paths to load as bundles for policy and data. Defaults to [\"${workspaceFolder}\"]." 133 | }, 134 | "opa.bundleMode": { 135 | "type": [ 136 | "boolean" 137 | ], 138 | "default": true, 139 | "description": "Enable treating the workspace as a bundle." 140 | }, 141 | "opa.schema": { 142 | "type": [ 143 | "string", 144 | "null" 145 | ], 146 | "default": null, 147 | "description": "Path to the schema file or directory. Defaults to null. If null, schema evaluation is disabled." 148 | }, 149 | "opa.strictMode": { 150 | "type": [ 151 | "boolean" 152 | ], 153 | "default": false, 154 | "description": "Enable strict-mode for the \"OPA: Check File Syntax\" command (OPA check)." 155 | }, 156 | "opa.languageServers": { 157 | "type": [ 158 | "array" 159 | ], 160 | "default": null, 161 | "description": "DEPRECATED: Language servers are now enabled by default. This setting will be ignored.", 162 | "deprecationMessage": "This setting is deprecated. Regal language server is now enabled by default when available." 163 | }, 164 | "opa.env": { 165 | "type": "object", 166 | "additionalProperties": { 167 | "type": "string" 168 | }, 169 | "default": {}, 170 | "description": "Environment variables passed to the process running OPA." 171 | }, 172 | "opa.formatter": { 173 | "type": [ 174 | "string" 175 | ], 176 | "default": "opa-fmt", 177 | "description": "The formatter to use for Rego. Supports: ['opa-fmt', 'opa-fmt-rego-v1', 'regal-fix']." 178 | } 179 | } 180 | }, 181 | "languages": [ 182 | { 183 | "id": "rego", 184 | "aliases": [ 185 | "Rego", 186 | "rego" 187 | ], 188 | "extensions": [ 189 | ".rego" 190 | ], 191 | "configuration": "./language-configuration.json" 192 | } 193 | ], 194 | "grammars": [ 195 | { 196 | "language": "rego", 197 | "scopeName": "source.rego", 198 | "path": "./syntaxes/Rego.tmLanguage" 199 | }, 200 | { 201 | "scopeName": "markdown.rego.codeblock", 202 | "path": "./syntaxes/markdown-inject.json", 203 | "injectTo": [ 204 | "text.html.markdown" 205 | ], 206 | "embeddedLanguages": { 207 | "meta.embedded.block.rego": "rego" 208 | } 209 | } 210 | ], 211 | "breakpoints": [ 212 | { 213 | "language": "rego" 214 | } 215 | ], 216 | "debuggers": [ 217 | { 218 | "type": "opa-debug", 219 | "label": "OPA Debug", 220 | "languages": [ 221 | "rego" 222 | ], 223 | "configurationAttributes": { 224 | "launch": { 225 | "properties": { 226 | "command": { 227 | "type": "string", 228 | "description": "The OPA command to run. E.g. 'eval', 'test'.", 229 | "default": "eval" 230 | }, 231 | "query": { 232 | "type": "string", 233 | "default": "data" 234 | }, 235 | "dataPaths": { 236 | "type": "array", 237 | "items": { 238 | "type": "string" 239 | } 240 | }, 241 | "bundlePaths": { 242 | "type": "array", 243 | "items": { 244 | "type": "string" 245 | }, 246 | "description": "List of paths to load as bundles for policy and data. If not set, defaults to the value configured in 'opa.roots'. Can be set to the empty array [] to disable loading of bundles.", 247 | "default": [ 248 | "${workspaceFolder}" 249 | ] 250 | }, 251 | "input": { 252 | "type": "string" 253 | }, 254 | "inputPath": { 255 | "type": "string" 256 | }, 257 | "stopOnEntry": { 258 | "type": "boolean", 259 | "description": "Automatically stop after launch.", 260 | "default": true 261 | }, 262 | "stopOnFail": { 263 | "type": "boolean", 264 | "description": "Automatically stop on 'Fail' operations.", 265 | "default": false 266 | }, 267 | "stopOnResult": { 268 | "type": "boolean", 269 | "description": "Automatically stop when done.", 270 | "default": true 271 | }, 272 | "trace": { 273 | "type": "boolean", 274 | "description": "Enable logging of the Debug Adapter Protocol.", 275 | "default": true 276 | }, 277 | "enablePrint": { 278 | "type": "boolean", 279 | "description": "Enable print statements.", 280 | "default": true 281 | }, 282 | "logLevel": { 283 | "type": "string", 284 | "description": "Set the log level for log messages printed to the debug console. One of: 'debug', 'info', 'warn', 'error'. If not set, no log messages are printed." 285 | }, 286 | "ruleIndexing": { 287 | "type": "boolean", 288 | "description": "Enable rule indexing.", 289 | "default": false 290 | } 291 | } 292 | } 293 | } 294 | } 295 | ] 296 | }, 297 | "scripts": { 298 | "vscode:prepublish": "npm run compile", 299 | "compile": "tsc -p ./", 300 | "watch": "tsc -watch -p ./", 301 | "lint": "eslint .", 302 | "lint:fix": "eslint . --fix", 303 | "format": "npx dprint fmt", 304 | "format:check": "npx dprint check", 305 | "typecheck": "tsc --noEmit" 306 | }, 307 | "devDependencies": { 308 | "@microsoft/eslint-formatter-sarif": "^3.1.0", 309 | "@stylistic/eslint-plugin": "^5.6.1", 310 | "@types/command-exists": "^1.2.3", 311 | "@types/mocha": "10.0.10", 312 | "@types/node": "24.10.1", 313 | "@types/semver": "^7.7.1", 314 | "@types/vscode": "1.106.1", 315 | "cspell": "^9.4.0", 316 | "dprint": "^0.50.2", 317 | "eslint": "^9.39.1", 318 | "markdownlint-cli2": "^0.20.0", 319 | "typescript": "^5.9.3", 320 | "typescript-eslint": "^8.48.1", 321 | "@vscode/vsce": "^3.7.1" 322 | }, 323 | "dependencies": { 324 | "command-exists": "^1.2.9", 325 | "semver": "^7.7.3", 326 | "vscode-languageclient": "^9.0.1" 327 | }, 328 | "__metadata": { 329 | "id": "ab758a0c-5cb5-417e-99bf-12a6e16bb148", 330 | "publisherDisplayName": "Torin Sandall", 331 | "publisherId": "4052f3dc-ff54-4c3b-9ad2-44e7ac5b9f4d" 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | 4 | ## 0.20.0 5 | 6 | - docs: Update references to new Regal location #356 7 | - Dependency Updates: 8 | - GitHub Actions: 9 | - `actions/checkout`: `v4` → `v5` 10 | - `actions/setup-node`: `v4.0.3` → `v6.0.0` 11 | - `github/codeql-action/upload-sarif`: `v3` → `v4` 12 | - NPM: 13 | - `semver`: `^7.6.3` → `^7.7.3` 14 | - `@stylistic/eslint-plugin`: `^2.12.1` → `^5.5.0` 15 | - `@types/node`: `22.10.2` → `24.9.1` 16 | - `@types/vscode`: `1.96.0` → `1.105.0` 17 | - `cspell`: `^8.17.0` → `^9.2.2` 18 | - `dprint`: `^0.47.6` → `^0.50.2` 19 | - `eslint`: `^9.17.0` → `^9.38.0` 20 | - `typescript`: `^5.7.2` → `^5.9.3` 21 | - `typescript-eslint`: `^8.18.0` → `^8.46.2` 22 | - `@vscode/vsce`: `^3.2.1` → `^3.6.2` 23 | - VSCode Engine Requirement: 24 | - `vscode`: `^1.96.0` → `^1.105.0` 25 | 26 | 27 | ## 0.19.0 28 | 29 | - util: fix bug in replaceWorkspaceFolderPathVariable [#288](https://github.com/open-policy-agent/vscode-opa/pull/288) 30 | - Dependency updates: 31 | - `@stylistic/eslint-plugin`: `^2.9.0` -> `^2.12.1` 32 | - `@types/mocha`: `10.0.8` -> `10.0.10` 33 | - `@types/node`: `22.7.4` -> `22.10.2` 34 | - `@types/vscode`: `1.94.0` -> `1.96.0` 35 | - `cspell`: `^8.14.4` -> `^8.17.0` 36 | - `dprint`: `^0.47.2` -> `^0.47.6` 37 | - `eslint`: `^9.12.0` -> `^9.17.0` 38 | - `typescript`: `^5.6.2` -> `^5.7.2` 39 | - `typescript-eslint`: `^8.8.0` -> `^8.18.0` 40 | - `@vscode/vsce`: `^3.1.1` -> `^3.2.1` 41 | 42 | 43 | ## 0.18.0 44 | 45 | - lsp: Explicitly enable custom LSP features [#282](https://github.com/open-policy-agent/vscode-opa/pull/282) 46 | - Dependency updates: 47 | - @types/vscode 1.93.0 to 1.94.0 [#281](https://github.com/open-policy-agent/vscode-opa/pull/281) 48 | - vscode 1.93.0 to 1.94.0 [#281](https://github.com/open-policy-agent/vscode-opa/pull/281) 49 | - vsce 2.15.0 to 3.1.1 [#281](https://github.com/open-policy-agent/vscode-opa/pull/281) 50 | - cspell 8.14.2 to 8.14.4 [#271](https://github.com/open-policy-agent/vscode-opa/pull/271) 51 | - eslint 9.10.0 to 9.12.0 [#273](https://github.com/open-policy-agent/vscode-opa/pull/273), [#274](https://github.com/open-policy-agent/vscode-opa/pull/274), [#280](https://github.com/open-policy-agent/vscode-opa/pull/280) 52 | - typescript-eslint 8.5.0 to 8.8.0 [#270](https://github.com/open-policy-agent/vscode-opa/pull/270), [#276](https://github.com/open-policy-agent/vscode-opa/pull/276), [#277](https://github.com/open-policy-agent/vscode-opa/pull/277) 53 | - typescript 5.5.4 to 5.6.2 [#272](https://github.com/open-policy-agent/vscode-opa/pull/272) 54 | - @types/node 22.5.5 to 22.7.4 [#275](https://github.com/open-policy-agent/vscode-opa/pull/275) 55 | - @stylistic/eslint-plugin 2.8.0 to 2.9.0 [#279](https://github.com/open-policy-agent/vscode-opa/pull/279) 56 | 57 | 58 | ## 0.17.0 59 | 60 | - Debugger Adapter Protocol Support, see [documentation here](https://docs.styra.com/regal/debug-adapter) - [\#218](https://github.com/open-policy-agent/vscode-opa/pull/218), [\#263](https://github.com/open-policy-agent/vscode-opa/pull/263), [\#262](https://github.com/open-policy-agent/vscode-opa/pull/262) 61 | - Added PR checks (eslint, dprint, markdownlint, cspell, vsce package) \- [\#246](https://github.com/open-policy-agent/vscode-opa/pull/246), [\#268](https://github.com/open-policy-agent/vscode-opa/pull/268) 62 | - Dependency updates 63 | - typescript-eslint: From 8.1.0 to 8.5.0 64 | - stylelint/eslint-plugin: From 2.6.2 to 2.8.0 65 | - cspell: From 8.14.1 to 8.14.2 66 | - types/node: From 22.0.0 to 22.5.5 67 | - types/mocha: From 10.0.7 to 10.0.8 68 | - types/vscode: From 1.92.0 to 1.93.0 69 | 70 | 71 | ## 0.16.0 72 | 73 | - Improvements to support code lens evaluation of Rego - 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | - Allow setting alternative formatters - 82 | - Check for OS architecture when installing OPA CLI - 83 | thanks @tjons! 84 | 85 | 86 | ## 0.15.0 87 | 88 | - Add support for syntax highlighting for Rego embedded in Markdown code blocks using the `rego` language identifier. 89 | 90 | Additionally, the [latest release](https://github.com/StyraInc/regal/releases/tag/v0.22.0) of the Regal language server (v0.22.0) brings a number of features relevant to this extension: 91 | 92 | - Basic support for code completion in Rego policies 93 | - Warning message displayed when CRLF line endings are detected in a Rego file 94 | - Parser errors now displayed more prominently, making them easier to spot 95 | - Errors reported by OPA will now link directly to corresponding docs, making it easier to understand and resolve issues 96 | 97 | Make sure to update Regal to get the latest features! 98 | 99 | 100 | ## 0.14.0 101 | 102 | This update is focused on exposing the latest features of the Regal language server in the extension. 103 | 104 | 105 | ### Client Language Server Updates 106 | 107 | 108 | #### Quick Fixes for Diagnostics 109 | 110 | Building on the current diagnostics supported, [Code actions](https://code.visualstudio.com/docs/editor/refactoring) now offer a means to quickly remediate common issues. Currently Code Actions are available for the following linter violations: 111 | 112 | - [OPA-fmt](https://docs.styra.com/regal/rules/style/opa-fmt) 113 | - [use-rego-v1](https://docs.styra.com/regal/rules/imports/use-rego-v1) 114 | - [use-assignment-operator](https://docs.styra.com/regal/rules/style/use-assignment-operator) 115 | - [no-whitespace-comment](https://docs.styra.com/regal/rules/style/no-whitespace-comment) 116 | 117 | More fixes to come in future releases now that the fundamentals are in place. It's also now possible to go to the linter diagnostic documentation as a Code Action. 118 | 119 | 120 | #### Document & Workspace Symbols 121 | 122 | Rego symbols — such as packages, rules and functions, are now provided by the Regal Language server upon requests from an editor. This allows for a quick overview of the structure of a Rego project, and provides "breadcrumbs" to navigate the symbols of the currently open Rego document. 123 | 124 | Similar to Document Symbols, the language server is able to provide symbols for top-level packages, rule or function definitions in the workspace. 125 | 126 | 127 | #### Formatting and Goto Definition 128 | 129 | We are standardizing the functions of the Rego developer environment on the [Regal Language Server](https://docs.styra.com/regal/editor-support) implementation. This allows us to offer a standardized experience to all Rego developers, regardless of their preferred editor. [OPA format](https://github.com/StyraInc/regal/pull/630) and [Goto Definition](https://github.com/StyraInc/regal/pull/664) are now available as part of the language server and so users are encouraged to use the language server to access the currently supported option for these editor functions. See PRs [#156](https://github.com/open-policy-agent/vscode-opa/pull/156)& [#148](https://github.com/open-policy-agent/vscode-opa/pull/148) where the VS Code OPA extension is updated to use this language server. 130 | 131 | 132 | #### Folding Ranges 133 | 134 | Code folding ranges are also now supported in the Regal language server and can be used to collapse comments, rules and others ranges within Rego files. 135 | 136 | 137 | #### Other Updates 138 | 139 | - Enable connection message logging in debug mode [#147](https://github.com/open-policy-agent/vscode-opa/pull/147) 140 | - Name Regal's output panel "Regal" instead of "regal-ls" [#145](https://github.com/open-policy-agent/vscode-opa/pull/145) 141 | - When restarting Regal, reuse output panel [#157](https://github.com/open-policy-agent/vscode-opa/pull/157) 142 | - Linter configuration can be loaded from a workspace's parent directory (Regal [#650](https://github.com/StyraInc/regal/pull/650)) 143 | 144 | 145 | ### Dependency updates 146 | 147 | - Bump typescript from 5.4.3 to 5.4.4 [#134](https://github.com/open-policy-agent/vscode-opa/pull/134) 148 | - Bump @types/vscode from 1.87.0 to 1.88.0 [#135](https://github.com/open-policy-agent/vscode-opa/pull/135) 149 | - Bump @types/node from 20.12.4 to 20.12.5 [#137](https://github.com/open-policy-agent/vscode-opa/pull/137) 150 | - Bump @typescript-eslint/eslint-plugin from 7.5.0 to 7.6.0 [#140](https://github.com/open-policy-agent/vscode-opa/pull/140) 151 | - Bump @typescript-eslint/parser from 7.5.0 to 7.6.0 [#141](https://github.com/open-policy-agent/vscode-opa/pull/141) 152 | - Bump typescript-eslint from 7.5.0 to 7.6.0 [#142](https://github.com/open-policy-agent/vscode-opa/pull/142) 153 | - Bump @types/node from 20.12.5 to 20.12.6 [#143](https://github.com/open-policy-agent/vscode-opa/pull/143) 154 | - Bump @types/node from 20.12.6 to 20.12.7 [#146](https://github.com/open-policy-agent/vscode-opa/pull/146) 155 | - Bump typescript from 5.4.4 to 5.4.5 [#149](https://github.com/open-policy-agent/vscode-opa/pull/149) 156 | - Bump @Microsoft/eslint-formatter-sarif from 3.0.0 to 3.1.0 [#150](https://github.com/open-policy-agent/vscode-opa/pull/150) 157 | - Bump @typescript-eslint/parser from 7.6.0 to 7.7.0 [#152](https://github.com/open-policy-agent/vscode-opa/pull/152) 158 | - Bump typescript-eslint from 7.6.0 to 7.7.0 [#153](https://github.com/open-policy-agent/vscode-opa/pull/153) 159 | - Bump @stylistic/eslint-plugin from 1.7.0 to 1.7.2 [#154](https://github.com/open-policy-agent/vscode-opa/pull/154) 160 | 161 | 162 | ## 0.13.6 163 | 164 | - Bump @types/node from 20.12.3 to 20.12.4 #131 165 | 166 | 167 | ## 0.13.5 168 | 169 | - Add workflow for extension release automation #112 170 | - Fix eslint warnings #116 171 | - fix(docs): fix docs links #117 172 | - Add keywords to package.JSON #118 173 | - Add dependabot config, linters, metadata #119 174 | - Fix edge case in OPA.test.workspace activation #120 175 | - Dependabot updates #121, #122, #123, #126, #127 176 | - Update vscode engine #128 177 | - Add grammar definition for raw strings #130 178 | 179 | 180 | ## 0.13.4 181 | 182 | - Add configuration to set environment variables for OPA subprocess #103 183 | - Use .jsonc extension for output.JSON #104 184 | - Address issue in Regal LS startups #105 185 | - Fix for OPA: Evaluate Package fails on some systems #108 186 | - Allow ${workspaceFolder} to be used in env var values #110 187 | 188 | 189 | ## 0.13.0 190 | 191 | - Support for [Regal](https://docs.styra.com/regal) Language server. 192 | 193 | 194 | ## 0.12.0 195 | 196 | - Keyword highlighting for `if` and `contains` 197 | 198 | 199 | ## 0.11.0 200 | 201 | - Adding support for compiler strict-mode (`--strict`) for OPA `check` command 202 | - Adding support for Running OPA commands on single files outside of a workspace 203 | - Syntax highlighting of the `every` keyword 204 | 205 | 206 | ## 0.10.0 207 | 208 | - Resolve `${workspaceFolder}` variable in `opa.path` setting 209 | - Fixing issue where 'Check File Syntax' command produces no output 210 | - Fixing error thrown when `opa.path` setting not set 211 | - Adding `opa.schema` setting 212 | -------------------------------------------------------------------------------- /syntaxes/Rego.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | fileTypes 6 | 7 | Rego 8 | 9 | name 10 | Rego 11 | patterns 12 | 13 | 14 | include 15 | #comment 16 | 17 | 18 | include 19 | #keyword 20 | 21 | 22 | include 23 | #comparison-operators 24 | 25 | 26 | include 27 | #assignment-operators 28 | 29 | 30 | include 31 | #term 32 | 33 | 34 | repository 35 | 36 | call 37 | 38 | captures 39 | 40 | 1 41 | 42 | name 43 | support.function.any-method.rego 44 | 45 | 46 | match 47 | ([a-zA-Z_][a-zA-Z0-9_]*)\( 48 | name 49 | meta.function-call.rego 50 | 51 | comment 52 | 53 | captures 54 | 55 | 1 56 | 57 | name 58 | punctuation.definition.comment.rego 59 | 60 | 61 | match 62 | (#).*$\n? 63 | name 64 | comment.line.number-sign.rego 65 | 66 | constant 67 | 68 | match 69 | \b(?:true|false|null)\b 70 | name 71 | constant.language.rego 72 | 73 | keyword 74 | 75 | match 76 | (^|\s+)(?:(default|not|package|import|as|with|else|some|in|every|if|contains))\s+ 77 | name 78 | keyword.other.rego 79 | 80 | number 81 | 82 | match 83 | (?x: # turn on extended mode 84 | -? # an optional minus 85 | (?: 86 | 0 # a zero 87 | | # ...or... 88 | [1-9] # a 1-9 character 89 | \d* # followed by zero or more digits 90 | ) 91 | (?: 92 | (?: 93 | \. # a period 94 | \d+ # followed by one or more digits 95 | )? 96 | (?: 97 | [eE] # an e character 98 | [+-]? # followed by an option +/- 99 | \d+ # followed by one or more digits 100 | )? # make exponent optional 101 | )? # make decimal portion optional 102 | ) 103 | name 104 | constant.numeric.rego 105 | 106 | comparison-operators 107 | 108 | match 109 | \=\=|\!\=|>|<|<\=|>\=|\+|-|\*|%|/|\||& 110 | name 111 | keyword.operator.comparison.rego 112 | 113 | assignment-operators 114 | 115 | match 116 | :\=|\= 117 | name 118 | keyword.operator.assignment.rego 119 | 120 | interpolated-string-double 121 | 122 | begin 123 | (\$)(") 124 | beginCaptures 125 | 126 | 1 127 | 128 | name 129 | punctuation.definition.template-expression.begin.rego 130 | 131 | 2 132 | 133 | name 134 | punctuation.definition.string.begin.rego 135 | 136 | 137 | end 138 | " 139 | endCaptures 140 | 141 | 0 142 | 143 | name 144 | punctuation.definition.string.end.rego 145 | 146 | 147 | name 148 | string.template.rego 149 | patterns 150 | 151 | 152 | include 153 | #interpolation-expression 154 | 155 | 156 | include 157 | #string-escape 158 | 159 | 160 | include 161 | #interpolation-escape 162 | 163 | 164 | include 165 | #string-escape-invalid 166 | 167 | 168 | 169 | interpolated-string-raw 170 | 171 | begin 172 | (\$)(`) 173 | beginCaptures 174 | 175 | 1 176 | 177 | name 178 | punctuation.definition.template-expression.begin.rego 179 | 180 | 2 181 | 182 | name 183 | punctuation.definition.string.begin.rego 184 | 185 | 186 | end 187 | ` 188 | endCaptures 189 | 190 | 0 191 | 192 | name 193 | punctuation.definition.string.end.rego 194 | 195 | 196 | name 197 | string.template.rego 198 | patterns 199 | 200 | 201 | include 202 | #interpolation-expression 203 | 204 | 205 | 206 | interpolation-expression 207 | 208 | begin 209 | (?<!\\)\{ 210 | beginCaptures 211 | 212 | 0 213 | 214 | name 215 | punctuation.section.embedded.rego 216 | 217 | 218 | end 219 | \} 220 | endCaptures 221 | 222 | 0 223 | 224 | name 225 | punctuation.section.embedded.rego 226 | 227 | 228 | name 229 | meta.embedded.expression.rego 230 | patterns 231 | 232 | 233 | include 234 | #interpolation-expression-contents 235 | 236 | 237 | 238 | interpolation-expression-contents 239 | 240 | patterns 241 | 242 | 243 | include 244 | #comment 245 | 246 | 247 | include 248 | #constant 249 | 250 | 251 | include 252 | #string 253 | 254 | 255 | include 256 | #number 257 | 258 | 259 | include 260 | #call 261 | 262 | 263 | include 264 | #variable 265 | 266 | 267 | include 268 | #comparison-operators 269 | 270 | 271 | 272 | string-escape 273 | 274 | match 275 | (?x: # turn on extended mode 276 | \\ # a literal backslash 277 | (?: # ...followed by... 278 | ["\\/bfnrt] # one of these characters 279 | | # ...or... 280 | u # a u 281 | [0-9a-fA-F]{4} # and four hex digits 282 | ) 283 | ) 284 | name 285 | constant.character.escape.rego 286 | 287 | interpolation-escape 288 | 289 | match 290 | \\[{}] 291 | name 292 | constant.character.escape.rego 293 | 294 | string-escape-invalid 295 | 296 | match 297 | \\. 298 | name 299 | invalid.illegal.unrecognized-string-escape.rego 300 | 301 | string 302 | 303 | patterns 304 | 305 | 306 | include 307 | #interpolated-string-double 308 | 309 | 310 | include 311 | #interpolated-string-raw 312 | 313 | 314 | begin 315 | " 316 | beginCaptures 317 | 318 | 0 319 | 320 | name 321 | punctuation.definition.string.begin.rego 322 | 323 | 324 | end 325 | " 326 | endCaptures 327 | 328 | 0 329 | 330 | name 331 | punctuation.definition.string.end.rego 332 | 333 | 334 | name 335 | string.quoted.double.rego 336 | patterns 337 | 338 | 339 | include 340 | #string-escape 341 | 342 | 343 | include 344 | #string-escape-invalid 345 | 346 | 347 | 348 | 349 | begin 350 | ` 351 | beginCaptures 352 | 353 | 0 354 | 355 | name 356 | punctuation.definition.string.begin.rego 357 | 358 | 359 | end 360 | ` 361 | endCaptures 362 | 363 | 0 364 | 365 | name 366 | punctuation.definition.string.end.rego 367 | 368 | 369 | name 370 | string.other.raw.rego 371 | 372 | 373 | 374 | term 375 | 376 | patterns 377 | 378 | 379 | include 380 | #constant 381 | 382 | 383 | include 384 | #string 385 | 386 | 387 | include 388 | #number 389 | 390 | 391 | include 392 | #call 393 | 394 | 395 | include 396 | #variable 397 | 398 | 399 | 400 | variable 401 | 402 | match 403 | \b[[:alpha:]_][[:alnum:]_]*\b 404 | name 405 | meta.identifier.rego 406 | 407 | 408 | scopeName 409 | source.rego 410 | uuid 411 | 165D3571-322B-4C57-ABAD-9EB3922FB004 412 | 413 | 414 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/ls/clients/regal.ts: -------------------------------------------------------------------------------- 1 | import { relative } from "path"; 2 | import * as semver from "semver"; 3 | import { workspace } from "vscode"; 4 | import * as vscode from "vscode"; 5 | import { 6 | CloseAction, 7 | CloseHandlerResult, 8 | ErrorAction, 9 | ErrorHandlerResult, 10 | LanguageClient, 11 | LanguageClientOptions, 12 | Message, 13 | ServerOptions, 14 | State, 15 | } from "vscode-languageclient/node"; 16 | import { REGAL_CONFIG, resolveBinary } from "../../binaries"; 17 | import { 18 | evalResultDecorationType, 19 | evalResultTargetSuccessDecorationType, 20 | evalResultTargetUndefinedDecorationType, 21 | opaOutputChannel, 22 | removeDecorations, 23 | } from "../../extension"; 24 | 25 | let client: LanguageClient; 26 | let clientLock = false; 27 | const activeDebugSessions: Map = new Map(); 28 | 29 | export function resolveRegalPath() { 30 | return resolveBinary(REGAL_CONFIG, "regal"); 31 | } 32 | 33 | export function regalPath(): string { 34 | const binaryInfo = resolveRegalPath(); 35 | return binaryInfo.path || "regal"; 36 | } 37 | 38 | class debuggableMessageStrategy { 39 | handleMessage(message: Message, next: (message: Message) => any): any { 40 | // If the VSCODE_DEBUG_MODE environment variable is set to true, then 41 | // we can log the messages to the console for debugging purposes. 42 | if (process.env.VSCODE_DEBUG_MODE === "true") { 43 | const messageData = JSON.parse(JSON.stringify(message)); 44 | const method = messageData.method || "response"; 45 | console.log(method, JSON.stringify(messageData)); 46 | } 47 | 48 | return next(message); 49 | } 50 | } 51 | 52 | export function activateRegal() { 53 | if (clientLock) { 54 | return; 55 | } 56 | clientLock = true; 57 | 58 | const binaryInfo = resolveBinary(REGAL_CONFIG, "regal"); 59 | 60 | // Validate binary availability 61 | if (!binaryInfo.path) { 62 | clientLock = false; 63 | return; 64 | } 65 | 66 | if (binaryInfo.version === "missing") { 67 | return; 68 | } 69 | 70 | // Validate minimum version if specified 71 | if (REGAL_CONFIG.minimumVersion && semver.valid(binaryInfo.version)) { 72 | if (semver.lt(binaryInfo.version, REGAL_CONFIG.minimumVersion)) { 73 | opaOutputChannel.appendLine( 74 | `${REGAL_CONFIG.name}: service could not be started - version ${binaryInfo.version} is below minimum ${REGAL_CONFIG.minimumVersion}`, 75 | ); 76 | return; 77 | } 78 | } 79 | 80 | // Log startup information 81 | if (binaryInfo.source === "configured" && binaryInfo.originalPath) { 82 | opaOutputChannel.appendLine( 83 | `${REGAL_CONFIG.name}: starting service with ${binaryInfo.originalPath} (${binaryInfo.path}) version ${binaryInfo.version}`, 84 | ); 85 | } else { 86 | opaOutputChannel.appendLine( 87 | `${REGAL_CONFIG.name}: starting service with system ${REGAL_CONFIG.configKey} version ${binaryInfo.version}`, 88 | ); 89 | } 90 | 91 | const serverOptions: ServerOptions = { 92 | command: binaryInfo.path!, 93 | args: ["language-server"], 94 | }; 95 | 96 | const clientOptions: LanguageClientOptions = { 97 | documentSelector: [{ scheme: "file", language: "rego" }], 98 | outputChannel: opaOutputChannel, 99 | traceOutputChannel: opaOutputChannel, 100 | revealOutputChannelOn: 0, 101 | connectionOptions: { 102 | messageStrategy: new debuggableMessageStrategy(), 103 | }, 104 | errorHandler: { 105 | error: (error: Error, message: Message, _count: number): ErrorHandlerResult => { 106 | console.error(error); 107 | console.error(message); 108 | return { 109 | action: ErrorAction.Continue, 110 | }; 111 | }, 112 | closed: (): CloseHandlerResult => { 113 | console.error("client closed"); 114 | return { 115 | action: CloseAction.DoNotRestart, 116 | }; 117 | }, 118 | }, 119 | synchronize: { 120 | fileEvents: [ 121 | workspace.createFileSystemWatcher("**/*.rego"), 122 | workspace.createFileSystemWatcher("**/.regal/config.yaml"), 123 | ], 124 | }, 125 | diagnosticPullOptions: { 126 | onChange: true, 127 | onSave: true, 128 | }, 129 | initializationOptions: { 130 | formatter: vscode.workspace.getConfiguration("opa").get("formatter", "opa-fmt"), 131 | // These options are passed to the Regal language server to signal the 132 | // capabilities of the client. Since VSCode and vscode-opa supports both 133 | // inline evaluation results and live debugging, both are enabled and are 134 | // not configurable. 135 | evalCodelensDisplayInline: true, 136 | enableDebugCodelens: true, 137 | }, 138 | }; 139 | 140 | client = new LanguageClient( 141 | "regal", 142 | "Regal LSP client", 143 | serverOptions, 144 | clientOptions, 145 | ); 146 | 147 | client.onRequest("regal/showEvalResult", handleRegalShowEvalResult); 148 | client.onRequest("regal/startDebugging", handleDebug); 149 | 150 | vscode.debug.onDidTerminateDebugSession((session) => { 151 | activeDebugSessions.delete(session.name); 152 | }); 153 | 154 | client.start(); 155 | } 156 | 157 | export function deactivateRegal(): Thenable | undefined { 158 | clientLock = false; 159 | if (!client) { 160 | return undefined; 161 | } 162 | 163 | return client.stop(); 164 | } 165 | 166 | export function isRegalRunning(): boolean { 167 | return client && client.state === State.Running; 168 | } 169 | 170 | export function restartRegal() { 171 | // Check if Regal binary is available before attempting restart 172 | const binaryInfo = resolveBinary(REGAL_CONFIG, "regal"); 173 | if (!binaryInfo.path || binaryInfo.version === "missing") { 174 | opaOutputChannel.appendLine("Error: Cannot restart Regal language server - Regal binary is not available"); 175 | return; 176 | } 177 | 178 | // Only restart if Regal is currently running or if we have a client instance 179 | if (!client) { 180 | opaOutputChannel.appendLine("Starting Regal language server..."); 181 | activateRegal(); 182 | return; 183 | } 184 | 185 | opaOutputChannel.appendLine("Restarting Regal language server..."); 186 | 187 | const stopPromise = deactivateRegal(); 188 | 189 | if (stopPromise) { 190 | stopPromise.then(() => { 191 | setTimeout(() => { 192 | activateRegal(); 193 | }, 100); 194 | }); 195 | } else { 196 | setTimeout(() => { 197 | activateRegal(); 198 | }, 100); 199 | } 200 | } 201 | 202 | interface ShowEvalResultParams { 203 | line: number; 204 | result: EvalResult; 205 | // package or a rule name 206 | target: string; 207 | // only used when target is a package 208 | package: string; 209 | // only used when target is a rule name, contains a list of rule head locations 210 | rule_head_locations: ShowEvalResultParamsLocation[]; 211 | } 212 | 213 | interface ShowEvalResultParamsLocation { 214 | row: number; 215 | col: number; 216 | } 217 | 218 | interface EvalResult { 219 | value: any; 220 | isUndefined: boolean; 221 | printOutput: { 222 | [file: string]: { 223 | [line: number]: [text: string[]]; 224 | }; 225 | }; 226 | } 227 | 228 | function handleDebug(params: vscode.DebugConfiguration) { 229 | const activeEditor = vscode.window.activeTextEditor; 230 | if (!activeEditor) { 231 | return; 232 | } 233 | 234 | if (activeDebugSessions.has(params.name)) { 235 | vscode.window.showErrorMessage("Debug session for '" + params.name + "' already active"); 236 | return; 237 | } 238 | 239 | vscode.debug.startDebugging(undefined, params).then((success) => { 240 | if (success) { 241 | activeDebugSessions.set(params.name, undefined); 242 | } 243 | }); 244 | } 245 | 246 | function handleRegalShowEvalResult(params: ShowEvalResultParams) { 247 | const activeEditor = vscode.window.activeTextEditor; 248 | if (!activeEditor) return; 249 | 250 | // Before setting a new decoration, remove all previous decorations 251 | removeDecorations(); 252 | 253 | const { attachmentMessage, hoverMessage } = createMessages(params); 254 | 255 | const decorationOptions: vscode.DecorationOptions[] = []; 256 | const targetDecorationOptions: vscode.DecorationOptions[] = []; 257 | 258 | const truncateThreshold = 100; 259 | 260 | if (params.target === "package") { 261 | handlePackageDecoration( 262 | params, 263 | activeEditor, 264 | decorationOptions, 265 | targetDecorationOptions, 266 | attachmentMessage, 267 | hoverMessage, 268 | truncateThreshold, 269 | ); 270 | } else if (params.rule_head_locations.length > 0) { 271 | handleRuleHeadsDecoration( 272 | params, 273 | activeEditor, 274 | decorationOptions, 275 | targetDecorationOptions, 276 | attachmentMessage, 277 | hoverMessage, 278 | truncateThreshold, 279 | ); 280 | } 281 | 282 | handlePrintOutputDecoration(params, activeEditor, decorationOptions, truncateThreshold); 283 | 284 | const wf = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); 285 | 286 | for (const [uri, items] of Object.entries(params.result.printOutput)) { 287 | let path; 288 | if (wf) { 289 | path = relative(wf.uri.fsPath, vscode.Uri.parse(uri).fsPath); 290 | } else { 291 | path = vscode.Uri.parse(uri).fsPath; 292 | } 293 | 294 | Object.keys(items).map(Number).forEach((line) => { 295 | const lineItems = items[line]; 296 | if (lineItems) { 297 | opaOutputChannel.appendLine(`🖨️ ${path}:${line} => ${lineItems.join(" => ")}`); 298 | } 299 | }); 300 | } 301 | 302 | // Always set the base decoration, containing the result message and after text 303 | activeEditor.setDecorations(evalResultDecorationType, decorationOptions); 304 | 305 | // Set decoration type based on whether the result is undefined 306 | const targetDecorationType = params.result.isUndefined 307 | ? evalResultTargetUndefinedDecorationType 308 | : evalResultTargetSuccessDecorationType; 309 | activeEditor.setDecorations(targetDecorationType, targetDecorationOptions); 310 | } 311 | 312 | function createMessages(params: ShowEvalResultParams) { 313 | let attachmentMessage = params.result.value; 314 | let hoverMessage = params.result.value; 315 | const hoverTitle = "### Evaluation Result\n\n"; 316 | 317 | if (params.result.isUndefined) { 318 | // Handle rule result 319 | attachmentMessage = "undefined"; 320 | hoverMessage = hoverTitle + makeCode("text", attachmentMessage); 321 | } else if (typeof params.result.value === "object") { 322 | // Handle objects (including arrays) 323 | const formattedValue = JSON.stringify(params.result.value, null, 2); 324 | attachmentMessage = formattedValue.replace(/\n\s*/g, " ") 325 | .replace(/(\{|\[)\s/g, "$1") 326 | .replace(/\s(\}|\])/g, "$1"); 327 | const code = makeCode("json", formattedValue); 328 | hoverMessage = hoverTitle + (code.length > 100000 ? formattedValue : code); 329 | } else { 330 | // Handle strings and other types simple types 331 | if (typeof params.result.value === "string") { 332 | attachmentMessage = `"${params.result.value.replace(/ /g, "\u00a0")}"`; 333 | } 334 | hoverMessage = hoverTitle + makeCode("json", attachmentMessage); 335 | } 336 | 337 | return { attachmentMessage, hoverMessage }; 338 | } 339 | 340 | function handlePackageDecoration( 341 | params: ShowEvalResultParams, 342 | activeEditor: vscode.TextEditor, 343 | decorationOptions: vscode.DecorationOptions[], 344 | targetDecorationOptions: vscode.DecorationOptions[], 345 | attachmentMessage: string, 346 | hoverMessage: string, 347 | truncateThreshold: number, 348 | ) { 349 | const line = params.line - 1; 350 | const documentLine = activeEditor.document.lineAt(line); 351 | const lineLength = documentLine.text.length; 352 | 353 | // To avoid horizontal scroll for large outputs, we ask users to hover for the full result 354 | if (lineLength + attachmentMessage.length > truncateThreshold) { 355 | const suffix = "... (hover for result)"; 356 | attachmentMessage = attachmentMessage.substring(0, truncateThreshold - lineLength - suffix.length) + suffix; 357 | } 358 | 359 | decorationOptions.push(createDecoration(line, lineLength, hoverMessage, attachmentMessage)); 360 | 361 | const packageIndex = documentLine.text.indexOf(params.package); 362 | const startChar = packageIndex > 0 ? packageIndex : 0; 363 | const endChar = packageIndex > 0 ? packageIndex + params.package.length : lineLength; 364 | 365 | // Highlight only the target name with a color, displayed in addition to the whole line decoration 366 | targetDecorationOptions.push({ 367 | range: new vscode.Range(new vscode.Position(line, startChar), new vscode.Position(line, endChar)), 368 | }); 369 | } 370 | 371 | function handleRuleHeadsDecoration( 372 | params: ShowEvalResultParams, 373 | activeEditor: vscode.TextEditor, 374 | decorationOptions: vscode.DecorationOptions[], 375 | targetDecorationOptions: vscode.DecorationOptions[], 376 | attachmentMessage: string, 377 | hoverMessage: string, 378 | truncateThreshold: number, 379 | ) { 380 | params.rule_head_locations.forEach((location) => { 381 | const line = location.row - 1; 382 | const documentLine = activeEditor.document.lineAt(line); 383 | const lineLength = documentLine.text.length; 384 | 385 | // To avoid horizontal scroll for large outputs, we ask users to hover for the full result 386 | if (lineLength + attachmentMessage.length > truncateThreshold) { 387 | const suffix = "... (hover for result)"; 388 | attachmentMessage = attachmentMessage.substring(0, truncateThreshold - lineLength - suffix.length) + suffix; 389 | } 390 | 391 | decorationOptions.push(createDecoration(line, lineLength, hoverMessage, attachmentMessage)); 392 | 393 | const startChar = location.col - 1; 394 | const endChar = documentLine.text.includes(params.target) 395 | ? startChar + params.target.length 396 | : findEndChar(documentLine.text, lineLength); 397 | 398 | // Highlight only the target name with a color, displayed in addition to the whole line decoration 399 | targetDecorationOptions.push({ 400 | range: new vscode.Range(new vscode.Position(line, startChar), new vscode.Position(line, endChar)), 401 | }); 402 | }); 403 | } 404 | 405 | function handlePrintOutputDecoration( 406 | params: ShowEvalResultParams, 407 | activeEditor: vscode.TextEditor, 408 | decorationOptions: vscode.DecorationOptions[], 409 | truncateThreshold: number, 410 | ) { 411 | // TODO: display print output in any file from the params map! 412 | // Currently only print output in the current editor is shown 413 | const printOutput = params.result.printOutput[activeEditor.document.uri.toString()]; 414 | if (!printOutput) { 415 | return; 416 | } 417 | 418 | Object.keys(printOutput).map(Number).forEach((line) => { 419 | const lineOutput = printOutput[line]; 420 | if (!lineOutput) return; 421 | 422 | const lineLength = activeEditor.document.lineAt(line).text.length; 423 | const joinedLines = lineOutput.join("\n"); 424 | 425 | // Pre-block formatting fails if there are over 100k chars 426 | const hoverText = joinedLines.length < 100000 ? makeCode("text", joinedLines) : joinedLines; 427 | const hoverMessage = "### Print Output\n\n" + hoverText; 428 | 429 | let attachmentMessage = ` 🖨️ => ${lineOutput.join(" => ")}`; 430 | if (lineLength + attachmentMessage.length > truncateThreshold) { 431 | const suffix = "... (hover for result)"; 432 | attachmentMessage = attachmentMessage.substring(0, truncateThreshold - lineLength - suffix.length) + suffix; 433 | } 434 | 435 | decorationOptions.push({ 436 | range: new vscode.Range(new vscode.Position(line - 1, 0), new vscode.Position(line - 1, lineLength)), 437 | hoverMessage: hoverMessage, 438 | renderOptions: { 439 | after: { 440 | contentText: attachmentMessage, 441 | color: new vscode.ThemeColor("editorLineNumber.foreground"), 442 | }, 443 | }, 444 | }); 445 | }); 446 | } 447 | 448 | function createDecoration( 449 | line: number, 450 | lineLength: number, 451 | hoverMessage: string, 452 | attachmentMessage: string, 453 | ): vscode.DecorationOptions { 454 | // Create base decoration options for a line 455 | return { 456 | range: new vscode.Range(new vscode.Position(line, 0), new vscode.Position(line, lineLength)), 457 | hoverMessage: hoverMessage, 458 | renderOptions: { 459 | after: { 460 | contentText: ` => ${attachmentMessage}`, 461 | color: new vscode.ThemeColor("editorLineNumber.foreground"), 462 | }, 463 | }, 464 | }; 465 | } 466 | 467 | function findEndChar(text: string, lineLength: number): number { 468 | // Find the end character position, stopping at the first [ or . character as a fallback 469 | for (let i = 0; i < lineLength; i++) { 470 | if (text[i] === "[" || text[i] === ".") { 471 | return i; 472 | } 473 | } 474 | return lineLength; 475 | } 476 | 477 | // makeCode returns a markdown code block with the given language and code 478 | function makeCode(lang: string, code: string) { 479 | return "```" + lang + "\n" + code + "\n```"; 480 | } 481 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import * as fs from "fs"; 4 | import * as path from "path"; 5 | import * as vscode from "vscode"; 6 | 7 | import { installBinary, OPA_CONFIG, REGAL_CONFIG, resolveBinary } from "./binaries"; 8 | import { activateDebugger } from "./da/activate"; 9 | import { activateRegal, isRegalRunning, restartRegal } from "./ls/clients/regal"; 10 | import * as opa from "./opa"; 11 | import { getPrettyTime } from "./util"; 12 | 13 | export const opaOutputChannel = vscode.window.createOutputChannel("OPA & Regal"); 14 | 15 | export class JSONProvider implements vscode.TextDocumentContentProvider { 16 | private _onDidChange = new vscode.EventEmitter(); 17 | private content = ""; 18 | 19 | public provideTextDocumentContent(_uri: vscode.Uri): string { 20 | return this.content; 21 | } 22 | 23 | get onDidChange(): vscode.Event { 24 | return this._onDidChange.event; 25 | } 26 | 27 | public set(uri: vscode.Uri, note: string, output: any) { 28 | this.content = note; 29 | if (output !== undefined) { 30 | this.content += "\n" + JSON.stringify(output, undefined, 2); 31 | } 32 | this._onDidChange.fire(uri); 33 | } 34 | } 35 | 36 | export function activate(context: vscode.ExtensionContext) { 37 | vscode.window.onDidChangeActiveTextEditor(showCoverageOnEditorChange, null, context.subscriptions); 38 | vscode.workspace.onDidChangeTextDocument(removeDecorationsOnDocumentChange, null, context.subscriptions); 39 | 40 | activateCheckFile(context); 41 | activateCoverWorkspace(context); 42 | activateEvalPackage(context); 43 | activateEvalSelection(context); 44 | activateEvalCoverage(context); 45 | activateTestWorkspace(context); 46 | activateTraceSelection(context); 47 | activateProfileSelection(context); 48 | activatePartialSelection(context); 49 | activateRestartRegalCommand(context); 50 | 51 | // check for missing binaries and prompt to install them 52 | checkMissingBinaries(); 53 | // start Regal language server 54 | activateRegal(); 55 | // activate the debugger 56 | activateDebugger(context); 57 | 58 | // this will trigger the prompt to install OPA if missing, rather than waiting til on save 59 | // the manual running of a command 60 | opa.runWithStatus("opa", ["version"], "", (_code: number, _stderr: string, _stdout: string) => {}); 61 | 62 | vscode.workspace.onDidChangeConfiguration((_event) => { 63 | // activateRegal is run here to catch newly installed language servers, 64 | // after their paths are updated. 65 | activateRegal(); 66 | activateDebugger(context); 67 | }); 68 | } 69 | 70 | // this is the decoration type for the eval result covering the whole line 71 | export const evalResultDecorationType = vscode.window.createTextEditorDecorationType({ 72 | isWholeLine: true, 73 | after: { 74 | textDecoration: "none", 75 | fontWeight: "normal", 76 | fontStyle: "normal", 77 | }, 78 | }); 79 | 80 | // decoration type for the eval result covering only the rule name when the result is defined 81 | export const evalResultTargetSuccessDecorationType = vscode.window.createTextEditorDecorationType({ 82 | isWholeLine: false, 83 | backgroundColor: new vscode.ThemeColor("diffEditor.insertedTextBackground"), 84 | }); 85 | 86 | // decoration type for the eval result covering only the rule name when the result is undefined 87 | export const evalResultTargetUndefinedDecorationType = vscode.window.createTextEditorDecorationType({ 88 | isWholeLine: false, 89 | backgroundColor: new vscode.ThemeColor("inputValidation.warningBackground"), 90 | }); 91 | 92 | // remove all decorations from the active editor using the known types of decorations 93 | export function removeDecorations() { 94 | Object.keys(fileCoverage).forEach((fileName) => { 95 | vscode.window.visibleTextEditors.forEach((value) => { 96 | if (value.document.fileName.endsWith(fileName)) { 97 | value.setDecorations(coveredHighlight, []); 98 | value.setDecorations(notCoveredHighlight, []); 99 | } 100 | }); 101 | }); 102 | 103 | fileCoverage = {}; 104 | 105 | vscode.window.visibleTextEditors.forEach((value) => { 106 | value.setDecorations(evalResultDecorationType, []); 107 | value.setDecorations(evalResultTargetSuccessDecorationType, []); 108 | value.setDecorations(evalResultTargetUndefinedDecorationType, []); 109 | }); 110 | } 111 | 112 | const outputUri = vscode.Uri.parse(`json:output.jsonc`); 113 | 114 | const coveredHighlight = vscode.window.createTextEditorDecorationType({ 115 | backgroundColor: "rgba(64,128,64,0.5)", 116 | isWholeLine: true, 117 | }); 118 | 119 | const notCoveredHighlight = vscode.window.createTextEditorDecorationType({ 120 | backgroundColor: "rgba(128,64,64,0.5)", 121 | isWholeLine: true, 122 | }); 123 | 124 | interface UntypedObject { 125 | [key: string]: any; 126 | } 127 | 128 | let fileCoverage: UntypedObject = {}; 129 | 130 | function showCoverageOnEditorChange(editor: vscode.TextEditor | undefined) { 131 | if (!editor) { 132 | return; 133 | } 134 | showCoverageForEditor(editor); 135 | } 136 | 137 | function removeDecorationsOnDocumentChange(e: vscode.TextDocumentChangeEvent) { 138 | // output:extension-output is the output channel for the extensions 139 | // and should not be used for clearing decorations 140 | if (e.document.uri.toString().startsWith("output:extension-output")) { 141 | return; 142 | } 143 | 144 | // output URI is the URI of the JSON file used for the eval result output 145 | if (`${e.document.uri}` === `${outputUri}`) { 146 | return; 147 | } 148 | 149 | removeDecorations(); 150 | } 151 | 152 | function showCoverageForEditor(_editor: vscode.TextEditor) { 153 | Object.keys(fileCoverage).forEach((fileName) => { 154 | vscode.window.visibleTextEditors.forEach((value) => { 155 | if (value.document.fileName.endsWith(fileName)) { 156 | value.setDecorations(coveredHighlight, fileCoverage[fileName].covered); 157 | value.setDecorations(notCoveredHighlight, fileCoverage[fileName].notCovered); 158 | } 159 | }); 160 | }); 161 | } 162 | 163 | function showCoverageForWindow() { 164 | vscode.window.visibleTextEditors.forEach((value) => { 165 | showCoverageForEditor(value); 166 | }); 167 | } 168 | 169 | function setFileCoverage(result: any) { 170 | Object.keys(result.files).forEach((fileName) => { 171 | const report = result.files[fileName]; 172 | if (!report) { 173 | return; 174 | } 175 | let covered = []; 176 | if (report.covered !== undefined) { 177 | covered = report.covered.map((range: any) => { 178 | return new vscode.Range(range.start.row - 1, 0, range.end.row - 1, 1000); 179 | }); 180 | } 181 | let notCovered = []; 182 | if (report.not_covered !== undefined) { 183 | notCovered = report.not_covered.map((range: any) => { 184 | return new vscode.Range(range.start.row - 1, 0, range.end.row - 1, 1000); 185 | }); 186 | } 187 | fileCoverage[fileName] = { 188 | covered: covered, 189 | notCovered: notCovered, 190 | }; 191 | }); 192 | } 193 | 194 | function setEvalOutput(provider: JSONProvider, uri: vscode.Uri, stderr: string, result: any, inputPath: string) { 195 | if (stderr !== "") { 196 | opaOutputShow(stderr); 197 | } 198 | 199 | let inputMessage: string; 200 | if (inputPath === "") { 201 | inputMessage = "no input file"; 202 | } else { 203 | inputMessage = inputPath.replace("file://", ""); 204 | inputMessage = vscode.workspace.asRelativePath(inputMessage); 205 | } 206 | if (result.result === undefined) { 207 | provider.set( 208 | outputUri, 209 | `// No results found. Took ${ 210 | getPrettyTime(result.metrics.timer_rego_query_eval_ns) 211 | }. Used ${inputMessage} as input.`, 212 | undefined, 213 | ); 214 | } else { 215 | let output: any; 216 | if (result.result[0].bindings === undefined) { 217 | output = result.result.map((x: any) => x.expressions.map((x: any) => x.value)); 218 | } else { 219 | output = result.result.map((x: any) => x.bindings); 220 | } 221 | provider.set( 222 | uri, 223 | `// Found ${result.result.length} result${result.result.length === 1 ? "" : "s"} in ${ 224 | getPrettyTime(result.metrics.timer_rego_query_eval_ns) 225 | } using ${inputMessage} as input.`, 226 | output, 227 | ); 228 | } 229 | } 230 | 231 | function activateCheckFile(context: vscode.ExtensionContext) { 232 | const checkRegoFile = () => { 233 | const editor = vscode.window.activeTextEditor; 234 | if (!editor) { 235 | return; 236 | } 237 | const doc = editor.document; 238 | 239 | // Only check rego files 240 | if (doc.languageId === "rego") { 241 | const args: string[] = ["check"]; 242 | 243 | ifInWorkspace(() => { 244 | if (opa.canUseBundleFlags()) { 245 | args.push("--bundle"); 246 | } 247 | args.push(...opa.getRoots()); 248 | args.push(...opa.getSchemaParams()); 249 | }, () => { 250 | args.push(doc.uri.fsPath); 251 | }); 252 | 253 | if (opa.canUseStrictFlag()) { 254 | args.push("--strict"); 255 | } 256 | 257 | opa.runWithStatus("opa", args, "", (_code: number, stderr: string, _stdout: string) => { 258 | const output = stderr; 259 | if (output.trim() !== "") { 260 | opaOutputShowError(output); 261 | } 262 | }); 263 | } 264 | }; 265 | 266 | const checkRegoFileOnSave = () => { 267 | if (checkOnSaveEnabled()) { 268 | checkRegoFile(); 269 | } 270 | }; 271 | 272 | const checkFileCommand = vscode.commands.registerCommand("opa.check.file", checkRegoFile); 273 | // Need to use onWillSave instead of onDidSave because there's a weird race condition 274 | // that causes the callback to get called twice when we prompt for installing OPA 275 | vscode.workspace.onWillSaveTextDocument(checkRegoFileOnSave, null, context.subscriptions); 276 | 277 | context.subscriptions.push(checkFileCommand); 278 | } 279 | 280 | function activateCoverWorkspace(context: vscode.ExtensionContext) { 281 | const coverWorkspaceCommand = vscode.commands.registerCommand("opa.test.coverage.workspace", () => { 282 | const editor = vscode.window.activeTextEditor; 283 | if (!editor) { 284 | return; 285 | } 286 | 287 | for (const fileName in fileCoverage) { 288 | if (editor.document.fileName.endsWith(fileName)) { 289 | removeDecorations(); 290 | return; 291 | } 292 | } 293 | 294 | fileCoverage = {}; 295 | 296 | const args: string[] = ["test", "--coverage", "--format", "json"]; 297 | 298 | ifInWorkspace(() => { 299 | if (opa.canUseBundleFlags()) { 300 | args.push("--bundle"); 301 | } 302 | 303 | args.push(...opa.getRoots()); 304 | }, () => { 305 | args.push(editor.document.uri.fsPath); 306 | }); 307 | 308 | opa.run("opa", args, "", (_, result) => { 309 | setFileCoverage(result); 310 | showCoverageForWindow(); 311 | }, opaOutputShowError); 312 | }); 313 | 314 | context.subscriptions.push(coverWorkspaceCommand); 315 | } 316 | 317 | function activateEvalPackage(context: vscode.ExtensionContext) { 318 | const provider = new JSONProvider(); 319 | const registration = vscode.workspace.registerTextDocumentContentProvider(outputUri.scheme, provider); 320 | 321 | const evalPackageCommand = vscode.commands.registerCommand( 322 | "opa.eval.package", 323 | onActiveWorkspaceEditor(outputUri, (editor: vscode.TextEditor, _inWorkspace: boolean) => { 324 | opa.parse("opa", opa.getDataDir(editor.document.uri), (pkg: string, _: string[]) => { 325 | const { inputPath, args } = createOpaEvalArgs(editor, pkg); 326 | args.push("--metrics"); 327 | 328 | opa.run("opa", args, "data." + pkg, (stderr, result) => { 329 | setEvalOutput(provider, outputUri, stderr, result, inputPath); 330 | }, opaOutputShowError); 331 | }, (error: string) => { 332 | opaOutputShowError(error); 333 | }); 334 | }), 335 | ); 336 | 337 | context.subscriptions.push(evalPackageCommand, registration); 338 | } 339 | 340 | function activateEvalSelection(context: vscode.ExtensionContext) { 341 | const provider = new JSONProvider(); 342 | const registration = vscode.workspace.registerTextDocumentContentProvider(outputUri.scheme, provider); 343 | 344 | const evalSelectionCommand = vscode.commands.registerCommand( 345 | "opa.eval.selection", 346 | onActiveWorkspaceEditor(outputUri, (editor: vscode.TextEditor) => { 347 | opa.parse("opa", opa.getDataDir(editor.document.uri), (pkg: string, imports: string[]) => { 348 | const { inputPath, args } = createOpaEvalArgs(editor, pkg, imports); 349 | args.push("--metrics"); 350 | 351 | const text = editor.document.getText(editor.selection); 352 | 353 | opa.run("opa", args, text, (stderr, result) => { 354 | setEvalOutput(provider, outputUri, stderr, result, inputPath); 355 | }, opaOutputShowError); 356 | }, (error: string) => { 357 | opaOutputShowError(error); 358 | }); 359 | }), 360 | ); 361 | 362 | context.subscriptions.push(evalSelectionCommand, registration); 363 | } 364 | 365 | function activateEvalCoverage(context: vscode.ExtensionContext) { 366 | const provider = new JSONProvider(); 367 | const registration = vscode.workspace.registerTextDocumentContentProvider(outputUri.scheme, provider); 368 | 369 | const evalCoverageCommand = vscode.commands.registerCommand( 370 | "opa.eval.coverage", 371 | onActiveWorkspaceEditor(outputUri, (editor: vscode.TextEditor) => { 372 | for (const fileName in fileCoverage) { 373 | if (editor.document.fileName.endsWith(fileName)) { 374 | removeDecorations(); 375 | return; 376 | } 377 | } 378 | 379 | fileCoverage = {}; 380 | 381 | opa.parse("opa", opa.getDataDir(editor.document.uri), (pkg: string, imports: string[]) => { 382 | const { inputPath, args } = createOpaEvalArgs(editor, pkg, imports); 383 | args.push("--metrics"); 384 | args.push("--coverage"); 385 | 386 | const text = editor.document.getText(editor.selection); 387 | 388 | opa.run("opa", args, text, (stderr, result) => { 389 | setEvalOutput(provider, outputUri, stderr, result, inputPath); 390 | setFileCoverage(result.coverage); 391 | showCoverageForWindow(); 392 | }, opaOutputShowError); 393 | }, (error: string) => { 394 | opaOutputShowError(error); 395 | }); 396 | }), 397 | ); 398 | 399 | context.subscriptions.push(evalCoverageCommand, registration); 400 | } 401 | 402 | function activateTestWorkspace(context: vscode.ExtensionContext) { 403 | const testWorkspaceCommand = vscode.commands.registerCommand("opa.test.workspace", () => { 404 | opaOutputChannel.show(true); 405 | opaOutputChannel.clear(); 406 | 407 | const args: string[] = ["test"]; 408 | 409 | args.push("--verbose"); 410 | 411 | ifInWorkspace(() => { 412 | if (opa.canUseBundleFlags()) { 413 | args.push("--bundle"); 414 | } 415 | args.push(...opa.getRoots()); 416 | }, () => { 417 | const editor = vscode.window.activeTextEditor; 418 | if (!editor) { 419 | return; 420 | } 421 | args.push(editor.document.uri.fsPath); 422 | }); 423 | 424 | opa.runWithStatus("opa", args, "", (code: number, stderr: string, stdout: string) => { 425 | if (code === 0 || code === 2) { 426 | opaOutputChannel.append(stdout); 427 | } else { 428 | opaOutputShowError(stderr); 429 | } 430 | }); 431 | }); 432 | 433 | context.subscriptions.push(testWorkspaceCommand); 434 | } 435 | 436 | function activateTraceSelection(context: vscode.ExtensionContext) { 437 | const traceSelectionCommand = vscode.commands.registerCommand("opa.trace.selection", () => { 438 | const editor = vscode.window.activeTextEditor; 439 | if (!editor) { 440 | return; 441 | } 442 | const text = editor.document.getText(editor.selection); 443 | 444 | opa.parse("opa", opa.getDataDir(editor.document.uri), (pkg: string, imports: string[]) => { 445 | const { args } = createOpaEvalArgs(editor, pkg, imports); 446 | args.push("--format", "pretty"); 447 | args.push("--explain", "full"); 448 | 449 | opa.runWithStatus("opa", args, text, (code: number, stderr: string, stdout: string) => { 450 | opaOutputChannel.show(true); 451 | opaOutputChannel.clear(); 452 | 453 | if (code === 0 || code === 2) { 454 | opaOutputChannel.append(stdout); 455 | } else { 456 | opaOutputShowError(stderr); 457 | } 458 | }); 459 | }, (error: string) => { 460 | opaOutputShowError(error); 461 | }); 462 | }); 463 | 464 | context.subscriptions.push(traceSelectionCommand); 465 | } 466 | 467 | function activateProfileSelection(context: vscode.ExtensionContext) { 468 | const profileSelectionCommand = vscode.commands.registerCommand("opa.profile.selection", () => { 469 | const editor = vscode.window.activeTextEditor; 470 | if (!editor) { 471 | return; 472 | } 473 | const text = editor.document.getText(editor.selection); 474 | 475 | opa.parse("opa", opa.getDataDir(editor.document.uri), (pkg: string, imports: string[]) => { 476 | opaOutputChannel.show(true); 477 | opaOutputChannel.clear(); 478 | 479 | const { args } = createOpaEvalArgs(editor, pkg, imports); 480 | args.push("--profile"); 481 | args.push("--format", "pretty"); 482 | 483 | opa.runWithStatus("opa", args, text, (code: number, stderr: string, stdout: string) => { 484 | if (code === 0 || code === 2) { 485 | opaOutputChannel.append(stdout); 486 | } else { 487 | opaOutputShowError(stderr); 488 | } 489 | }); 490 | }, (error: string) => { 491 | opaOutputShowError(error); 492 | }); 493 | }); 494 | 495 | context.subscriptions.push(profileSelectionCommand); 496 | } 497 | 498 | function activatePartialSelection(context: vscode.ExtensionContext) { 499 | const partialSelectionCommand = vscode.commands.registerCommand("opa.partial.selection", () => { 500 | const editor = vscode.window.activeTextEditor; 501 | if (!editor) { 502 | return; 503 | } 504 | const text = editor.document.getText(editor.selection); 505 | 506 | opa.parse("opa", opa.getDataDir(editor.document.uri), (pkg: string, imports: string[]) => { 507 | const depsArgs = ["deps", "--format", "json"]; 508 | 509 | ifInWorkspace(() => { 510 | depsArgs.push(...opa.getRootParams()); 511 | }, () => { 512 | depsArgs.push("--data", editor.document.uri.fsPath); 513 | }); 514 | 515 | depsArgs.push("data." + pkg); 516 | 517 | opa.run("opa", depsArgs, "", (_, result: any) => { 518 | const refs = result.base.map((ref: any) => opa.refToString(ref)); 519 | refs.push("input"); 520 | vscode.window.showQuickPick(refs).then((selection: string | undefined) => { 521 | if (selection !== undefined) { 522 | opaOutputChannel.show(true); 523 | opaOutputChannel.clear(); 524 | 525 | const { args } = createOpaEvalArgs(editor, pkg, imports); 526 | args.push("--partial"); 527 | args.push("--format", "pretty"); 528 | args.push("--unknowns", selection); 529 | 530 | opa.runWithStatus("opa", args, text, (code: number, stderr: string, stdout: string) => { 531 | if (code === 0 || code === 2) { 532 | opaOutputChannel.append(stdout); 533 | } else { 534 | opaOutputShowError(stderr); 535 | } 536 | }); 537 | } 538 | }); 539 | }, (msg) => { 540 | opaOutputShowError(msg); 541 | }); 542 | }, (error: string) => { 543 | opaOutputShowError(error); 544 | }); 545 | }); 546 | 547 | context.subscriptions.push(partialSelectionCommand); 548 | } 549 | 550 | function activateRestartRegalCommand(context: vscode.ExtensionContext) { 551 | const restartRegalCommand = vscode.commands.registerCommand("opa.regal.restart", () => { 552 | restartRegal(); 553 | }); 554 | 555 | context.subscriptions.push(restartRegalCommand); 556 | } 557 | 558 | function onActiveWorkspaceEditor( 559 | forURI: vscode.Uri, 560 | cb: (editor: vscode.TextEditor, inWorkspace: boolean) => void, 561 | ): () => void { 562 | return async () => { 563 | // TODO(tsandall): test non-workspace mode. I don't know if this plugin 564 | // will work if a single file is loaded. Certain features may not work 565 | // but many can. 566 | const editor = vscode.window.activeTextEditor; 567 | if (!editor) { 568 | vscode.window.showErrorMessage("No active editor"); 569 | return; 570 | } 571 | 572 | const inWorkspace = !!vscode.workspace.workspaceFolders; 573 | 574 | // Execute the callback first to populate content 575 | cb(editor, !!inWorkspace); 576 | 577 | // Then open the read-only document beside the current editor. 578 | // If no read-only document exists yet, create a new one. If one exists, 579 | // re-use it. 580 | try { 581 | const doc = await vscode.workspace.openTextDocument(forURI); 582 | const found = vscode.window.visibleTextEditors.find((ed: vscode.TextEditor) => { 583 | return ed.document.uri === doc.uri; 584 | }); 585 | 586 | if (found === undefined) { 587 | await vscode.window.showTextDocument(doc, vscode.ViewColumn.Beside, true); 588 | } 589 | } catch (error) { 590 | console.error("Failed to open output document:", error); 591 | } 592 | }; 593 | } 594 | 595 | let informAboutWorkspace = true; 596 | const informAboutWorkspaceOption = "Don't show this tip again"; 597 | 598 | function ifInWorkspace(yes: () => void, no: () => void = () => {}) { 599 | if (vscode.workspace.workspaceFolders) { 600 | yes(); 601 | } else { 602 | if (informAboutWorkspace) { 603 | vscode.window.showInformationMessage( 604 | "You're editing a single file. Open it inside a workspace to include " 605 | + "any relative modules and schemas in the OPA commands you run.", 606 | informAboutWorkspaceOption, 607 | ).then((selection: string | undefined) => { 608 | if (selection === informAboutWorkspaceOption) { 609 | informAboutWorkspace = false; 610 | } 611 | }); 612 | } 613 | no(); 614 | } 615 | } 616 | 617 | export function deactivate() { 618 | } 619 | 620 | function opaOutputShow(msg: string) { 621 | opaOutputChannel.clear(); 622 | opaOutputChannel.append(msg); 623 | opaOutputChannel.show(true); 624 | } 625 | 626 | function opaOutputShowError(error: string) { 627 | opaOutputChannel.clear(); 628 | opaOutputChannel.append(formatErrors(error)); 629 | opaOutputChannel.show(true); 630 | } 631 | 632 | function formatErrors(error: string): string { 633 | try { 634 | const output = JSON.parse(error); 635 | let errors; 636 | if (output.error !== undefined) { 637 | if (!Array.isArray(output.error)) { 638 | errors = [output.error]; 639 | } else { 640 | errors = output.error; 641 | } 642 | } else if (output.errors !== undefined) { 643 | errors = output.errors; 644 | } 645 | const msg = []; 646 | for (let i = 0; i < errors.length; i++) { 647 | let location_prefix; 648 | if (errors[i].location.file !== "") { 649 | location_prefix = `${errors[i].location.file}:${errors[i].location.row}`; 650 | } else { 651 | location_prefix = ``; 652 | } 653 | msg.push(`${location_prefix}: ${errors[i].code}: ${errors[i].message}`); 654 | } 655 | return msg.join("\n"); 656 | } catch (_) { 657 | return error; 658 | } 659 | } 660 | 661 | function checkOnSaveEnabled() { 662 | return vscode.workspace.getConfiguration("opa").get("checkOnSave"); 663 | } 664 | 665 | export function existsSync(path: string): boolean { 666 | const parsed = vscode.Uri.parse(path); 667 | 668 | if (parsed.scheme === "file") { 669 | return fs.existsSync(parsed.fsPath); 670 | } 671 | 672 | return fs.existsSync(path); 673 | } 674 | 675 | export function getInputPath(): string { 676 | // look for input.json at the active editor's directory, or the workspace directory 677 | 678 | const activeDir = path.dirname(vscode.window.activeTextEditor!.document.uri.fsPath); 679 | let parsed = vscode.Uri.file(activeDir); 680 | 681 | // If we're in a workspace, and there is no sibling input.json to the actively edited file, look for the file in the workspace root 682 | if ( 683 | !!vscode.workspace.workspaceFolders 684 | && vscode.workspace.workspaceFolders.length > 0 685 | && !fs.existsSync(path.join(activeDir, "input.json")) 686 | ) { 687 | const firstWorkspaceFolder = vscode.workspace.workspaceFolders[0]; 688 | if (firstWorkspaceFolder) { 689 | parsed = firstWorkspaceFolder.uri; 690 | } 691 | } 692 | 693 | // If the rootDir is a file:// URL then just append /input.json onto the 694 | // end. Otherwise use the path.join function to get a platform-specific file 695 | // path returned. 696 | const rootDir = opa.getDataDir(parsed); 697 | 698 | if (parsed.scheme === "file") { 699 | return parsed.toString() + "/input.json"; 700 | } 701 | 702 | return path.join(rootDir, "input.json"); 703 | } 704 | 705 | function createOpaEvalArgs( 706 | editor: vscode.TextEditor, 707 | pkg: string, 708 | imports: string[] = [], 709 | ): { inputPath: string; args: string[] } { 710 | const args: string[] = ["eval"]; 711 | 712 | args.push("--stdin"); 713 | args.push("--package", pkg); 714 | 715 | let inputPath = getInputPath(); 716 | if (existsSync(inputPath)) { 717 | args.push("--input", inputPath); 718 | } else { 719 | inputPath = ""; 720 | } 721 | 722 | imports.forEach((x: string) => { 723 | args.push("--import", x); 724 | }); 725 | 726 | ifInWorkspace(() => { 727 | args.push(...opa.getRootParams()); 728 | args.push(...opa.getSchemaParams()); 729 | }, () => { 730 | args.push("--data", editor.document.uri.fsPath); 731 | }); 732 | 733 | return { inputPath, args }; 734 | } 735 | 736 | // Check for missing binaries and prompt to install them 737 | function checkMissingBinaries() { 738 | const missingBinaries: Array<{ config: typeof REGAL_CONFIG | typeof OPA_CONFIG; name: string }> = []; 739 | 740 | if (!resolveBinary(OPA_CONFIG, "opa").path) { 741 | missingBinaries.push({ config: OPA_CONFIG, name: "OPA" }); 742 | } 743 | 744 | if (!resolveBinary(REGAL_CONFIG, "regal").path) { 745 | missingBinaries.push({ config: REGAL_CONFIG, name: "Regal" }); 746 | } 747 | 748 | if (missingBinaries.length > 0) { 749 | const names = missingBinaries.map(b => b.name).join(" and "); 750 | const message = `${names} ${ 751 | missingBinaries.length === 1 ? "is" : "are" 752 | } needed but not installed. Would you like to install ${missingBinaries.length === 1 ? "it" : "them"}?`; 753 | 754 | vscode.window.showInformationMessage(message, "Install") 755 | .then(async (selection) => { 756 | if (selection === "Install") { 757 | for (const binary of missingBinaries) { 758 | try { 759 | await installBinary(binary.config, opaOutputChannel); 760 | } catch (error) { 761 | console.error(`Failed to install ${binary.name}:`, error); 762 | } 763 | } 764 | 765 | // if any Regal binary was installed and if we need to start/restart it server 766 | const hasRegalBinary = missingBinaries.some(b => b.config === REGAL_CONFIG); 767 | if (hasRegalBinary) { 768 | if (isRegalRunning()) { 769 | opaOutputChannel.appendLine("Regal is running, restarting with new binary..."); 770 | restartRegal(); 771 | } else { 772 | opaOutputChannel.appendLine("Starting Regal with newly installed binary..."); 773 | activateRegal(); 774 | } 775 | } 776 | } 777 | }); 778 | } 779 | } 780 | --------------------------------------------------------------------------------