1 Commits

Author SHA1 Message Date
Henrik Jess Nielsen
0025043999 backup: uncommitted changes from MAC-M9FQ0900T3 2026-05-17 15:52:31 2026-05-17 15:52:31 +02:00
18 changed files with 497 additions and 4982 deletions

View File

@@ -12,6 +12,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends curl unzip ca-c
COPY scripts/download_bicep_ls.sh /scripts/ COPY scripts/download_bicep_ls.sh /scripts/
RUN chmod +x /scripts/download_bicep_ls.sh && BICEP_VERSION=${BICEP_VERSION} /scripts/download_bicep_ls.sh RUN chmod +x /scripts/download_bicep_ls.sh && BICEP_VERSION=${BICEP_VERSION} /scripts/download_bicep_ls.sh
# Download Azure DevOps pipeline schema for YAML task completions
RUN curl -f -o /azdo-pipeline-schema.json \
"https://raw.githubusercontent.com/microsoft/azure-pipelines-vscode/main/service-schema.json"
# ── Stage 2: Python wheel build ─────────────────────────────────────────────── # ── Stage 2: Python wheel build ───────────────────────────────────────────────
FROM python:3.12-slim AS builder FROM python:3.12-slim AS builder
@@ -41,6 +45,7 @@ RUN apt-get update \
# Copy Bicep Language Server (baked in at build time — no volume needed) # Copy Bicep Language Server (baked in at build time — no volume needed)
COPY --from=bicep-downloader /opt/bicep-langserver /opt/bicep-langserver COPY --from=bicep-downloader /opt/bicep-langserver /opt/bicep-langserver
COPY --from=bicep-downloader /azdo-pipeline-schema.json /azdo-pipeline-schema.json
# Install Python package and dependencies # Install Python package and dependencies
COPY --from=builder /dist/*.whl /tmp/ COPY --from=builder /dist/*.whl /tmp/

View File

@@ -14,49 +14,17 @@ supports LSP WebSocket transport.
## IntelliJ IDEA setup (LSP4IJ) ## IntelliJ IDEA setup (LSP4IJ)
Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then add language servers: Install the [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin, then:
### Bicep
1. **Settings → Languages & Frameworks → LSP → Language Servers → +** 1. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Bicep` - Name: `iLSP Bicep`
- Server type: `Command` (not WebSocket — see note below) - Server type: `WebSocket`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/bicep` - URL: `wss://ilsp.i80.dk/bicep`
- Arguments: (leave blank)
- File pattern: `*.bicep;*.bicepparam`
### YAML (Azure DevOps + GitHub Actions) 2. **Add a file-type mapping** under the new server entry:
- File type: `Bicep` (or pattern `*.bicep`)
2. **Settings → Languages & Frameworks → LSP → Language Servers → +** 3. **Disable Azure Toolkit Bicep completions** — this is critical if you have the
- Name: `iLSP YAML`
- Server type: `Command`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/yaml`
- Arguments: (leave blank)
- File pattern: `*.yaml;*.yml;azure-pipelines.yml`
### Python (optional)
3. **Settings → Languages & Frameworks → LSP → Language Servers → +**
- Name: `iLSP Python`
- Server type: `Command`
- Command: `/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh wss://ilsp.i80.dk/python`
- Arguments: (leave blank)
- File pattern: `*.py`
> **Note**: LSP4IJ's WebSocket mode doesn't handle LSP Content-Length framing correctly,
> causing "starting..." to hang or "Stream closed" errors. Use `lsp_bridge_debug.sh` wrapper
> which calls `lsp_bridge.py` and logs errors to `/tmp/lsp_bridge_debug.log` for debugging.
### Troubleshooting
If the language server doesn't start, check the debug log:
```bash
tail -f /tmp/lsp_bridge_debug.log
```
### Disable conflicting plugins
**Disable Azure Toolkit Bicep completions** — this is critical if you have the
Azure Toolkit plugin installed. Without this step, IDEA merges completions from Azure Toolkit plugin installed. Without this step, IDEA merges completions from
both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`, both LSP4IJ and Azure Toolkit, resulting in noisy suggestions (`resource`,
`projectName`, Bicep schema types, etc.) appearing alongside iLSP results. `projectName`, Bicep schema types, etc.) appearing alongside iLSP results.
@@ -75,20 +43,13 @@ tail -f /tmp/lsp_bridge_debug.log
## Neovim setup ## Neovim setup
> **Important**: `vim.lsp.rpc.connect(url)` does **not** speak WebSocket — it's a raw
> TCP/pipe connector and will fail with `ENOENT` on a `wss://` URL (verified). Use
> `cmd = { ".../scripts/lsp_bridge_debug.sh", "wss://..." }` instead (stdio ↔
> WebSocket bridge, same one IntelliJ/LSP4IJ uses). Confirmed working end-to-end
> (32 module completions returned) with the config below.
```lua ```lua
-- In your LSP config (e.g. ~/.config/nvim/lua/lsp.lua) -- In your LSP config (e.g. ~/.config/nvim/lua/lsp.lua)
local BRIDGE = "/Users/lrihni/Projects/iLSP/scripts/lsp_bridge_debug.sh"
-- Bicep -- Bicep
vim.lsp.start({ vim.lsp.start({
name = "ilsp-bicep", name = "ilsp-bicep",
cmd = { BRIDGE, "wss://ilsp.i80.dk/bicep" }, cmd = vim.lsp.rpc.connect("wss://ilsp.i80.dk/bicep"),
root_dir = vim.fs.dirname(vim.fs.find({ "bicepconfig.json", ".git" }, { upward = true })[1]), root_dir = vim.fs.dirname(vim.fs.find({ "bicepconfig.json", ".git" }, { upward = true })[1]),
filetypes = { "bicep" }, filetypes = { "bicep" },
}) })
@@ -96,7 +57,7 @@ vim.lsp.start({
-- YAML (pipeline files) -- YAML (pipeline files)
vim.lsp.start({ vim.lsp.start({
name = "ilsp-yaml", name = "ilsp-yaml",
cmd = { BRIDGE, "wss://ilsp.i80.dk/yaml" }, cmd = vim.lsp.rpc.connect("wss://ilsp.i80.dk/yaml"),
root_dir = vim.fs.dirname(vim.fs.find({ ".git" }, { upward = true })[1]), root_dir = vim.fs.dirname(vim.fs.find({ ".git" }, { upward = true })[1]),
filetypes = { "yaml" }, filetypes = { "yaml" },
}) })
@@ -104,7 +65,7 @@ vim.lsp.start({
-- Python -- Python
vim.lsp.start({ vim.lsp.start({
name = "ilsp-python", name = "ilsp-python",
cmd = { BRIDGE, "wss://ilsp.i80.dk/python" }, cmd = vim.lsp.rpc.connect("wss://ilsp.i80.dk/python"),
root_dir = vim.fs.dirname(vim.fs.find({ "pyproject.toml", ".git" }, { upward = true })[1]), root_dir = vim.fs.dirname(vim.fs.find({ "pyproject.toml", ".git" }, { upward = true })[1]),
filetypes = { "python" }, filetypes = { "python" },
}) })
@@ -189,20 +150,12 @@ python3 scripts/sync_pipeline_templates.py # → pipeline_templates_catalog.js
python3 scripts/sync_iac_module_sources.py # → iac_source_catalog.json (from DevOpsMCP) python3 scripts/sync_iac_module_sources.py # → iac_source_catalog.json (from DevOpsMCP)
python3 scripts/sync_bicep_modules.py # → bicep_modules_catalog.json python3 scripts/sync_bicep_modules.py # → bicep_modules_catalog.json
# 2. Commit + push — Gitea Actions builds the image and Nomad redeploys it # 2. Push and deploy
git add bicep_modules_catalog.json iac_source_catalog.json pipeline_templates_catalog.json bash scripts/push_catalogs.sh
git commit -m "chore: refresh catalogs"
git push
``` ```
> **Note**: `scripts/push_catalogs.sh` and the `/reload` endpoint only take effect if The `/reload` endpoint on the running service reloads catalogs from volume-mounted
> `/data` is volume-mounted into the running container. **Production (`ilsp.nomad`) files if present, so you can update without a full redeploy for pipeline templates.
> currently has no `/data` mount** — the underlying Nomad client has raw Docker bind
> mounts disabled (`volumes are not enabled`), so `push_catalogs.sh` silently pushes
> to a path the container never sees, and `/reload` is a no-op there. `/data` only
> works for **local dev** via `make run-with-data`. In production, catalog updates
> always require a commit + redeploy (as above) until the client is configured with
> a proper Nomad `host_volume`.
--- ---

View File

@@ -13,70 +13,6 @@ Provides smart autocomplete on top of standard LSPs:
→ See **[EDITOR_SETUP.md](EDITOR_SETUP.md)** for editor configuration and a full feature overview. → See **[EDITOR_SETUP.md](EDITOR_SETUP.md)** for editor configuration and a full feature overview.
## LSP feature coverage
Full capability audit against production (verified 2026-08-14) — everything below
is confirmed working end-to-end over the live WebSocket endpoints, not just in
theory:
| Feature | Bicep (`/bicep`) | Python (`/python`) | YAML (`/yaml`) |
|---|---|---|---|
| Completions (+ internal catalogs) | ✅ | ✅ | ✅ |
| Hover | ✅ | ✅ | ✅ |
| Go to definition | ✅ | ✅ | — |
| Find references | ✅ | ✅ | — |
| Document symbols | ✅ | ✅ | — |
| Document formatting | ✅ | ✅ | — |
| Code actions | ✅ | ✅ | — |
| Signature help | ✅ *(iLSP-provided)* | ✅ | — |
| Folding range | ✅ *(iLSP-provided)* | ✅ | — |
| Workspace symbol | ✅ *(iLSP-provided)* | ❌ (pylsp doesn't implement it) | — |
| Diagnostics | ✅ | ✅ | ✅ |
Bicep.LangServer itself (v0.46.1, latest) doesn't implement signature help,
folding range, or workspace symbol — iLSP answers these three locally in the
proxy (`ilsp/bicep_lsp/local_features.py`) instead of forwarding to a backend
that would just error or return nothing.
### Confirmed examples (live against `wss://ilsp.i80.dk/bicep`)
**Role-array completion** — cursor inside `roles: [...]` returns all 682 Azure
built-in roles:
```
roles: ['KEY_VAULT_SECRETS_USER', 'STORAGE_BLOB_DATA_CONTRIBUTOR', <cursor>]
→ 682 items: ACCESS_REVIEW_OPERATOR_SERVICE_ROLE, ACRDELETE, ACRIMAGESIGNER, ACRPULL, ACRPUSH, ...
```
**Go to definition** — jumping from a variable usage to its declaration:
```bicep
var storageName = '${projectName}storage' # declared here (line 2)
resource sa '...' = {
name: storageName # cursor here jumps to line 2
}
```
**Find references** — from the `storageName` declaration, finds both places
it's used: the string interpolation `${projectName}storage` isn't a reference,
but the `resource sa { name: storageName }` property assignment is — returns
the declaration itself plus that 1 real usage (2 results total).
**Document symbols** — outline for a file with params/vars/resources/modules:
```
location, projectName, storageName, sa, roleAssignment, storageId
```
**Signature help** — cursor right after `resourceId(`:
```
resourceId(resourceType, resourceName1, resourceName2...)
^ active parameter highlighted
```
**Folding range** — every multi-line `{}`/`[]`/`()` block returned as a
collapsible region (nested blocks included).
**Workspace symbol** — searching `"storage"` across all open documents in the
editor session returns `storageName`, `storageId`, etc.
## Quick start ## Quick start
```bash ```bash
@@ -95,14 +31,9 @@ make health # curl health endpoint
## Updating catalogs ## Updating catalogs
Catalogs are baked into the Docker image at build time — commit + push to update
production (see [EDITOR_SETUP.md](EDITOR_SETUP.md#updating-catalogs) for the full
workflow and a note on why `push_catalogs.sh` / `/reload` don't take effect in prod).
```bash ```bash
python3 scripts/sync_pipeline_templates.py # scan AzDO + GHA template repos python3 scripts/sync_pipeline_templates.py # scan AzDO + GHA template repos
python3 scripts/sync_bicep_modules.py # from DevOpsMCP repo — needs az CLI + ACR access bash scripts/push_catalogs.sh # scp to server + /reload
git add *_catalog.json && git commit -m "chore: refresh catalogs" && git push
``` ```
## Deploy ## Deploy

File diff suppressed because it is too large Load Diff

View File

@@ -1,684 +0,0 @@
[
"ACCESS_REVIEW_OPERATOR_SERVICE_ROLE",
"ACRDELETE",
"ACRIMAGESIGNER",
"ACRPULL",
"ACRPUSH",
"ACRQUARANTINEREADER",
"ACRQUARANTINEWRITER",
"ADVISOR_RECOMMENDATIONS_CONTRIBUTOR_ASSESSMENTS_AND_REVIEWS",
"ADVISOR_REVIEWS_CONTRIBUTOR",
"ADVISOR_REVIEWS_READER",
"AGENTLESS_SCANNING_FOR_SERVERLESS_SCANNER_SERVICE_ROLE",
"AGFOOD_PLATFORM_DATASET_ADMIN",
"AGFOOD_PLATFORM_SENSOR_PARTNER_CONTRIBUTOR",
"AGFOOD_PLATFORM_SERVICE_ADMIN",
"AGFOOD_PLATFORM_SERVICE_CONTRIBUTOR",
"AGFOOD_PLATFORM_SERVICE_READER",
"ANYBUILD_BUILDER",
"API_MANAGEMENT_DEVELOPER_PORTAL_CONTENT_EDITOR",
"API_MANAGEMENT_SERVICE_CONTRIBUTOR",
"API_MANAGEMENT_SERVICE_OPERATOR_ROLE",
"API_MANAGEMENT_SERVICE_READER_ROLE",
"API_MANAGEMENT_SERVICE_WORKSPACE_API_DEVELOPER",
"API_MANAGEMENT_SERVICE_WORKSPACE_API_PRODUCT_MANAGER",
"API_MANAGEMENT_WORKSPACE_API_DEVELOPER",
"API_MANAGEMENT_WORKSPACE_API_PRODUCT_MANAGER",
"API_MANAGEMENT_WORKSPACE_CONTRIBUTOR",
"API_MANAGEMENT_WORKSPACE_READER",
"APPGW_FOR_CONTAINERS_CONFIGURATION_MANAGER",
"APPLICATION_GROUP_CONTRIBUTOR",
"APPLICATION_INSIGHTS_COMPONENT_CONTRIBUTOR",
"APPLICATION_INSIGHTS_SNAPSHOT_DEBUGGER",
"APP_COMPLIANCE_AUTOMATION_ADMINISTRATOR",
"APP_COMPLIANCE_AUTOMATION_READER",
"APP_CONFIGURATION_CONTRIBUTOR",
"APP_CONFIGURATION_DATA_OWNER",
"APP_CONFIGURATION_DATA_READER",
"APP_CONFIGURATION_DATA_SAS_USER",
"APP_CONFIGURATION_READER",
"APP_SERVICE_ENVIRONMENT_CONTRIBUTOR",
"ARC_GATEWAY_MANAGER",
"ATTESTATION_CONTRIBUTOR",
"ATTESTATION_READER",
"AUTOMATION_CONTRIBUTOR",
"AUTOMATION_JOB_OPERATOR",
"AUTOMATION_OPERATOR",
"AUTOMATION_RUNBOOK_OPERATOR",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_CONTRIBUTOR_PREVIEW",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_OWNER_PREVIEW",
"AUTONOMOUS_DEVELOPMENT_PLATFORM_DATA_READER_PREVIEW",
"AUTO_ACTIONS_CONTRIBUTOR",
"AVERE_CONTRIBUTOR",
"AVERE_OPERATOR",
"AVS_ON_FLEET_VIS_ROLE",
"AVS_ORCHESTRATOR_ROLE",
"AZUREML_COMPUTE_OPERATOR",
"AZUREML_DATA_SCIENTIST",
"AZUREML_METRICS_WRITER_PREVIEW",
"AZUREML_REGISTRY_USER",
"AZURE_AI_ACCOUNT_OWNER",
"AZURE_AI_ADMINISTRATOR",
"AZURE_AI_DEVELOPER",
"AZURE_AI_ENTERPRISE_NETWORK_CONNECTION_APPROVER",
"AZURE_AI_INFERENCE_DEPLOYMENT_OPERATOR",
"AZURE_AI_PROJECT_MANAGER",
"AZURE_AI_SAFETY_EVALUATOR",
"AZURE_AI_USER",
"AZURE_API_CENTER_COMPLIANCE_MANAGER",
"AZURE_API_CENTER_CREDENTIAL_ACCESS_READER",
"AZURE_API_CENTER_DATA_READER",
"AZURE_API_CENTER_SERVICE_CONTRIBUTOR",
"AZURE_API_CENTER_SERVICE_READER",
"AZURE_ARC_ENABLED_KUBERNETES_CLUSTER_USER_ROLE",
"AZURE_ARC_KUBERNETES_ADMIN",
"AZURE_ARC_KUBERNETES_CLUSTER_ADMIN",
"AZURE_ARC_KUBERNETES_VIEWER",
"AZURE_ARC_KUBERNETES_WRITER",
"AZURE_ARC_SCVMM_ADMINISTRATOR_ROLE",
"AZURE_ARC_SCVMM_PRIVATE_CLOUDS_ONBOARDING",
"AZURE_ARC_SCVMM_PRIVATE_CLOUD_USER",
"AZURE_ARC_SCVMM_VM_CONTRIBUTOR",
"AZURE_ARC_VMWARE_ADMINISTRATOR_ROLE",
"AZURE_ARC_VMWARE_PRIVATE_CLOUDS_ONBOARDING",
"AZURE_ARC_VMWARE_PRIVATE_CLOUD_USER",
"AZURE_ARC_VMWARE_VM_CONTRIBUTOR",
"AZURE_AUTOMANAGE_CONTRIBUTOR",
"AZURE_BACKUP_SNAPSHOT_CONTRIBUTOR",
"AZURE_BATCH_ACCOUNT_CONTRIBUTOR",
"AZURE_BATCH_ACCOUNT_READER",
"AZURE_BATCH_DATA_CONTRIBUTOR",
"AZURE_BATCH_JOB_SUBMITTER",
"AZURE_BATCH_SERVICE_ORCHESTRATION_ROLE",
"AZURE_BOT_SERVICE_CONTRIBUTOR_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_ADMINISTRATOR",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_MANAGEMENT_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_READER",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_SERVICE_ROLE",
"AZURE_CENTER_FOR_SAP_SOLUTIONS_SERVICE_ROLE_FOR_MANAGEMENT",
"AZURE_CONNECTED_MACHINE_ONBOARDING",
"AZURE_CONNECTED_MACHINE_RESOURCE_ADMINISTRATOR",
"AZURE_CONNECTED_MACHINE_RESOURCE_MANAGER",
"AZURE_CONNECTED_SQL_SERVER_ONBOARDING",
"AZURE_CONTAINERAPPS_SESSION_EXECUTOR",
"AZURE_CONTAINER_INSTANCES_CONTRIBUTOR_ROLE",
"AZURE_CONTAINER_REGISTRY_SECURE_SUPPLY_CHAIN_OPERATOR_SERVICE_ROLE",
"AZURE_CONTAINER_STORAGE_CONTRIBUTOR",
"AZURE_CONTAINER_STORAGE_OPERATOR",
"AZURE_CONTAINER_STORAGE_OWNER",
"AZURE_CUSTOMER_LOCKBOX_APPROVER_FOR_SUBSCRIPTION",
"AZURE_DEPLOYMENT_STACK_CONTRIBUTOR",
"AZURE_DEPLOYMENT_STACK_OWNER",
"AZURE_DEVICE_ONBOARDING_DISCOVERY_CONTRIBUTOR",
"AZURE_DEVICE_UPDATE_AGENT",
"AZURE_DIGITAL_TWINS_DATA_OWNER",
"AZURE_DIGITAL_TWINS_DATA_READER",
"AZURE_EDGE_HARDWARE_CENTER_ADMINISTRATOR",
"AZURE_EDGE_ON_SITE_DEPLOYMENT_ENGINEER",
"AZURE_EVENT_HUBS_DATA_OWNER",
"AZURE_EVENT_HUBS_DATA_RECEIVER",
"AZURE_EVENT_HUBS_DATA_SENDER",
"AZURE_EXTENSION_FOR_SQL_SERVER_DEPLOYMENT",
"AZURE_FILE_SYNC_ADMINISTRATOR",
"AZURE_FILE_SYNC_READER",
"AZURE_FRONT_DOOR_DOMAIN_CONTRIBUTOR",
"AZURE_FRONT_DOOR_DOMAIN_READER",
"AZURE_FRONT_DOOR_PROFILE_READER",
"AZURE_FRONT_DOOR_SECRET_CONTRIBUTOR",
"AZURE_FRONT_DOOR_SECRET_READER",
"AZURE_HYBRID_DATABASE_ADMINISTRATOR_READ_ONLY_SERVICE_ROLE",
"AZURE_IMPACT_INSIGHT_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_FLEET_MANAGER_HUB_AGENT_ROLE",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_ADMIN",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_ADMIN",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_CLUSTER_WRITER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_READER",
"AZURE_KUBERNETES_FLEET_MANAGER_RBAC_WRITER",
"AZURE_KUBERNETES_SERVICE_AGENT_POOL_MANAGER_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_ARC_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_CLUSTER_MONITORING_USER",
"AZURE_KUBERNETES_SERVICE_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CLUSTER_ADMIN_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CLUSTER_USER_ROLE",
"AZURE_KUBERNETES_SERVICE_HYBRID_CONTRIBUTOR_ROLE",
"AZURE_KUBERNETES_SERVICE_POLICY_ADD_ON_DEPLOYMENT",
"AZURE_KUBERNETES_SERVICE_RBAC_ADMIN",
"AZURE_KUBERNETES_SERVICE_RBAC_CLUSTER_ADMIN",
"AZURE_KUBERNETES_SERVICE_RBAC_READER",
"AZURE_KUBERNETES_SERVICE_RBAC_WRITER",
"AZURE_MACHINE_LEARNING_WORKSPACE_CONNECTION_SECRETS_READER",
"AZURE_MANAGED_GRAFANA_WORKSPACE_CONTRIBUTOR",
"AZURE_MAPS_CONTRIBUTOR",
"AZURE_MAPS_DATA_CONTRIBUTOR",
"AZURE_MAPS_DATA_READER",
"AZURE_MAPS_DATA_READ_AND_BATCH_ROLE",
"AZURE_MAPS_SEARCH_AND_RENDER_DATA_READER",
"AZURE_MESSAGING_CATALOG_DATA_OWNER",
"AZURE_MESSAGING_CONNECTORS_OWNER",
"AZURE_MONITOR_DASHBOARDS_WITH_GRAFANA_CONTRIBUTOR",
"AZURE_PROGRAMMABLE_CONNECTIVITY_GATEWAY_DATAPLANE_USER",
"AZURE_PROGRAMMABLE_CONNECTIVITY_GATEWAY_USER",
"AZURE_RED_HAT_OPENSHIFT_CLOUD_CONTROLLER_MANAGER",
"AZURE_RED_HAT_OPENSHIFT_CLUSTER_INGRESS_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_DISK_STORAGE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_FEDERATED_CREDENTIAL",
"AZURE_RED_HAT_OPENSHIFT_FILE_STORAGE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_CLUSTER_API_PROVIDER",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_CONTROL_PLANE_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_HOSTED_CONTROL_PLANES_SERVICE_MANAGED_IDENTITY",
"AZURE_RED_HAT_OPENSHIFT_IMAGE_REGISTRY_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_MACHINE_API_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_NETWORK_OPERATOR",
"AZURE_RED_HAT_OPENSHIFT_SERVICE_OPERATOR",
"AZURE_RELAY_LISTENER",
"AZURE_RELAY_OWNER",
"AZURE_RELAY_SENDER",
"AZURE_RESOURCE_BRIDGE_DEPLOYMENT_ROLE",
"AZURE_RESOURCE_NOTIFICATIONS_SYSTEM_TOPICS_SUBSCRIBER",
"AZURE_SERVICE_BUS_DATA_OWNER",
"AZURE_SERVICE_BUS_DATA_RECEIVER",
"AZURE_SERVICE_BUS_DATA_SENDER",
"AZURE_SPHERE_CONTRIBUTOR",
"AZURE_SPHERE_OWNER",
"AZURE_SPHERE_PUBLISHER",
"AZURE_SPHERE_READER",
"AZURE_SPRING_APPS_APPLICATION_CONFIGURATION_SERVICE_CONFIG_FILE_PATTERN_READER_ROLE",
"AZURE_SPRING_APPS_APPLICATION_CONFIGURATION_SERVICE_LOG_READER_ROLE",
"AZURE_SPRING_APPS_CONNECT_ROLE",
"AZURE_SPRING_APPS_JOB_EXECUTION_INSTANCE_LIST_ROLE",
"AZURE_SPRING_APPS_JOB_LOG_READER_ROLE",
"AZURE_SPRING_APPS_MANAGED_COMPONENTS_LOG_READER_ROLE",
"AZURE_SPRING_APPS_REMOTE_DEBUGGING_ROLE",
"AZURE_SPRING_APPS_SPRING_CLOUD_CONFIG_SERVER_LOG_READER_ROLE",
"AZURE_SPRING_APPS_SPRING_CLOUD_GATEWAY_LOG_READER_ROLE",
"AZURE_SPRING_CLOUD_CONFIG_SERVER_CONTRIBUTOR",
"AZURE_SPRING_CLOUD_CONFIG_SERVER_READER",
"AZURE_SPRING_CLOUD_DATA_READER",
"AZURE_SPRING_CLOUD_SERVICE_REGISTRY_CONTRIBUTOR",
"AZURE_SPRING_CLOUD_SERVICE_REGISTRY_READER",
"AZURE_STACK_HCI_ADMINISTRATOR",
"AZURE_STACK_HCI_CONNECTED_INFRAVMS",
"AZURE_STACK_HCI_DEVICE_MANAGEMENT_ROLE",
"AZURE_STACK_HCI_EDGE_MACHINE_CONTRIBUTOR_ROLE",
"AZURE_STACK_HCI_VM_CONTRIBUTOR",
"AZURE_STACK_HCI_VM_READER",
"AZURE_STACK_REGISTRATION_OWNER",
"AZURE_USAGE_BILLING_DATA_SENDER",
"AZURE_VM_MANAGED_IDENTITIES_RESTORE_CONTRIBUTOR",
"BACKUP_CONTRIBUTOR",
"BACKUP_MUA_ADMIN",
"BACKUP_MUA_OPERATOR",
"BACKUP_OPERATOR",
"BACKUP_READER",
"BAYER_AG_POWERED_SERVICES_CROP_ID_SOLUTION_USER_ROLE",
"BAYER_AG_POWERED_SERVICES_CWUM_SOLUTION",
"BAYER_AG_POWERED_SERVICES_FIELD_IMAGERY_SOLUTION_SERVICE_ROLE",
"BAYER_AG_POWERED_SERVICES_GDU_SOLUTION",
"BAYER_AG_POWERED_SERVICES_HISTORICAL_WEATHER_DATA_SOLUTION_USER_ROLE",
"BAYER_AG_POWERED_SERVICES_IMAGERY_SOLUTION",
"BAYER_AG_POWERED_SERVICES_SMART_BOUNDARY_SOLUTION_USER_ROLE",
"BILLING_READER",
"BIZTALK_CONTRIBUTOR",
"BLOCKCHAIN_MEMBER_NODE_ACCESS_PREVIEW",
"BLUEPRINT_CONTRIBUTOR",
"BLUEPRINT_OPERATOR",
"CARBON_OPTIMIZATION_READER",
"CDN_ENDPOINT_CONTRIBUTOR",
"CDN_ENDPOINT_READER",
"CDN_PROFILE_CONTRIBUTOR",
"CDN_PROFILE_READER",
"CHAOS_STUDIO_EXPERIMENT_CONTRIBUTOR",
"CHAOS_STUDIO_OPERATOR",
"CHAOS_STUDIO_READER",
"CHAOS_STUDIO_TARGET_CONTRIBUTOR",
"CLASSIC_NETWORK_CONTRIBUTOR",
"CLASSIC_STORAGE_ACCOUNT_CONTRIBUTOR",
"CLASSIC_STORAGE_ACCOUNT_KEY_OPERATOR_SERVICE_ROLE",
"CLASSIC_VIRTUAL_MACHINE_CONTRIBUTOR",
"CLEARDB_MYSQL_DB_CONTRIBUTOR",
"CLOUDTEST_CONTRIBUTOR_ROLE",
"COGNITIVE_SEARCH_SERVERLESS_DATA_CONTRIBUTOR_DEPRECATED",
"COGNITIVE_SEARCH_SERVERLESS_DATA_READER_DEPRECATED",
"COGNITIVE_SERVICES_CONTRIBUTOR",
"COGNITIVE_SERVICES_CUSTOM_VISION_CONTRIBUTOR",
"COGNITIVE_SERVICES_CUSTOM_VISION_DEPLOYMENT",
"COGNITIVE_SERVICES_CUSTOM_VISION_LABELER",
"COGNITIVE_SERVICES_CUSTOM_VISION_READER",
"COGNITIVE_SERVICES_CUSTOM_VISION_TRAINER",
"COGNITIVE_SERVICES_DATA_CONTRIBUTOR_PREVIEW",
"COGNITIVE_SERVICES_DATA_READER",
"COGNITIVE_SERVICES_FACE_CONTRIBUTOR",
"COGNITIVE_SERVICES_FACE_RECOGNIZER",
"COGNITIVE_SERVICES_IMMERSIVE_READER_USER",
"COGNITIVE_SERVICES_LANGUAGE_OWNER",
"COGNITIVE_SERVICES_LANGUAGE_READER",
"COGNITIVE_SERVICES_LANGUAGE_WRITER",
"COGNITIVE_SERVICES_LUIS_OWNER",
"COGNITIVE_SERVICES_LUIS_READER",
"COGNITIVE_SERVICES_LUIS_WRITER",
"COGNITIVE_SERVICES_METRICS_ADVISOR_ADMINISTRATOR",
"COGNITIVE_SERVICES_METRICS_ADVISOR_USER",
"COGNITIVE_SERVICES_OPENAI_CONTRIBUTOR",
"COGNITIVE_SERVICES_OPENAI_USER",
"COGNITIVE_SERVICES_QNA_MAKER_EDITOR",
"COGNITIVE_SERVICES_QNA_MAKER_READER",
"COGNITIVE_SERVICES_SPEECH_CONTRIBUTOR",
"COGNITIVE_SERVICES_SPEECH_USER",
"COGNITIVE_SERVICES_USAGES_READER",
"COGNITIVE_SERVICES_USER",
"COLLABORATIVE_DATA_CONTRIBUTOR",
"COLLABORATIVE_RUNTIME_OPERATOR",
"COMMUNICATION_AND_EMAIL_SERVICE_OWNER",
"COMMUNITY_CONTRIBUTOR_ROLE",
"COMMUNITY_OWNER_ROLE",
"COMMUNITY_READER_ROLE",
"COMPUTE_DIAGNOSTICS_ROLE",
"COMPUTE_FLEET_CONTRIBUTOR",
"COMPUTE_GALLERY_ARTIFACTS_PUBLISHER",
"COMPUTE_GALLERY_IMAGE_READER",
"COMPUTE_GALLERY_SHARING_ADMIN",
"COMPUTE_RECOMMENDATIONS_ROLE",
"CONNECTED_CLUSTER_MANAGED_IDENTITY_CHECKACCESS_READER",
"CONNECTOR_READER",
"CONTAINERAPP_READER",
"CONTAINER_APPS_CONNECTEDENVIRONMENTS_CONTRIBUTOR",
"CONTAINER_APPS_CONNECTEDENVIRONMENTS_READER",
"CONTAINER_APPS_CONTRIBUTOR",
"CONTAINER_APPS_JOBS_CONTRIBUTOR",
"CONTAINER_APPS_JOBS_OPERATOR",
"CONTAINER_APPS_JOBS_READER",
"CONTAINER_APPS_MANAGEDENVIRONMENTS_CONTRIBUTOR",
"CONTAINER_APPS_MANAGEDENVIRONMENTS_READER",
"CONTAINER_APPS_OPERATOR",
"CONTAINER_APPS_SESSIONPOOLS_CONTRIBUTOR",
"CONTAINER_APPS_SESSIONPOOLS_READER",
"CONTAINER_INSTANCES_CONTRIBUTOR",
"CONTAINER_REGISTRY_CACHE_RULE_ADMINISTRATOR",
"CONTAINER_REGISTRY_CACHE_RULE_READER",
"CONTAINER_REGISTRY_CONFIGURATION_READER_AND_DATA_ACCESS_CONFIGURATION_READER",
"CONTAINER_REGISTRY_CONTRIBUTOR_AND_DATA_ACCESS_CONFIGURATION_ADMINISTRATOR",
"CONTAINER_REGISTRY_CREDENTIAL_SET_ADMINISTRATOR",
"CONTAINER_REGISTRY_CREDENTIAL_SET_READER",
"CONTAINER_REGISTRY_DATA_IMPORTER_AND_DATA_READER",
"CONTAINER_REGISTRY_REPOSITORY_CATALOG_LISTER",
"CONTAINER_REGISTRY_REPOSITORY_CONTRIBUTOR",
"CONTAINER_REGISTRY_REPOSITORY_READER",
"CONTAINER_REGISTRY_REPOSITORY_WRITER",
"CONTAINER_REGISTRY_TASKS_CONTRIBUTOR",
"CONTAINER_REGISTRY_TRANSFER_PIPELINE_CONTRIBUTOR",
"CONTRIBUTOR",
"COSMOSBACKUPOPERATOR",
"COSMOSRESTOREOPERATOR",
"COSMOS_DB_ACCOUNT_READER_ROLE",
"COSMOS_DB_OPERATOR",
"COST_MANAGEMENT_CONTRIBUTOR",
"COST_MANAGEMENT_READER",
"CROSSCONNECTIONMANAGER",
"CROSSCONNECTIONREADER",
"CUSTOM_AUTOMATION_ROLE_ROOT",
"DATA_BOUNDARY_TENANT_ADMINISTRATOR",
"DATA_BOX_CONTRIBUTOR",
"DATA_BOX_READER",
"DATA_FACTORY_CONTRIBUTOR",
"DATA_LABELING_LABELER",
"DATA_LAKE_ANALYTICS_DEVELOPER",
"DATA_OPERATOR_FOR_MANAGED_DISKS",
"DATA_PURGER",
"DEDICATED_HOST_CONTRIBUTOR_ROLE",
"DEFENDER_CSPM_STORAGE_DATA_SCANNER",
"DEFENDER_CSPM_STORAGE_SCANNER_OPERATOR",
"DEFENDER_FOR_STORAGE_DATA_SCANNER",
"DEFENDER_FOR_STORAGE_SCANNER_OPERATOR",
"DEFENDER_KUBERNETES_AGENT_OPERATOR",
"DEID_BATCH_DATA_OWNER",
"DEID_BATCH_DATA_READER",
"DEID_DATA_OWNER",
"DEID_REALTIME_DATA_USER",
"DEPLOYMENT_ENVIRONMENTS_READER",
"DEPLOYMENT_ENVIRONMENTS_USER",
"DESKTOP_VIRTUALIZATION_APPLICATION_GROUP_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_APPLICATION_GROUP_READER",
"DESKTOP_VIRTUALIZATION_APP_ATTACH_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_HOST_POOL_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_HOST_POOL_READER",
"DESKTOP_VIRTUALIZATION_POWER_ON_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_POWER_ON_OFF_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_READER",
"DESKTOP_VIRTUALIZATION_SESSION_HOST_OPERATOR",
"DESKTOP_VIRTUALIZATION_USER",
"DESKTOP_VIRTUALIZATION_USER_SESSION_OPERATOR",
"DESKTOP_VIRTUALIZATION_VIRTUAL_MACHINE_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_WORKSPACE_CONTRIBUTOR",
"DESKTOP_VIRTUALIZATION_WORKSPACE_READER",
"DEVCENTER_DEV_BOX_USER",
"DEVCENTER_PROJECT_ADMIN",
"DEVICE_PROVISIONING_SERVICE_DATA_CONTRIBUTOR",
"DEVICE_PROVISIONING_SERVICE_DATA_READER",
"DEVICE_UPDATE_ADMINISTRATOR",
"DEVICE_UPDATE_CONTENT_ADMINISTRATOR",
"DEVICE_UPDATE_CONTENT_READER",
"DEVICE_UPDATE_DEPLOYMENTS_ADMINISTRATOR",
"DEVICE_UPDATE_DEPLOYMENTS_READER",
"DEVICE_UPDATE_READER",
"DEVTEST_LABS_USER",
"DICOM_DATA_OWNER",
"DICOM_DATA_READER",
"DISK_BACKUP_READER",
"DISK_ENCRYPTION_SET_OPERATOR_FOR_MANAGED_DISKS",
"DISK_POOL_OPERATOR",
"DISK_RESTORE_OPERATOR",
"DISK_SNAPSHOT_CONTRIBUTOR",
"DNS_RESOLVER_CONTRIBUTOR",
"DNS_ZONE_CONTRIBUTOR",
"DOCUMENTDB_ACCOUNT_CONTRIBUTOR",
"DOMAIN_SERVICES_CONTRIBUTOR",
"DOMAIN_SERVICES_READER",
"DURABLE_TASK_DATA_CONTRIBUTOR",
"DURABLE_TASK_DATA_READER",
"DURABLE_TASK_WORKER",
"EDGE_MANAGEMENT_COPILOT_USER",
"ELASTIC_SAN_NETWORK_ADMIN",
"ELASTIC_SAN_OWNER",
"ELASTIC_SAN_READER",
"ELASTIC_SAN_SNAPSHOT_EXPORTER",
"ELASTIC_SAN_VOLUME_GROUP_OWNER",
"ELASTIC_SAN_VOLUME_IMPORTER",
"ENCLAVE_APPROVER_ROLE",
"ENCLAVE_CONTRIBUTOR_ROLE",
"ENCLAVE_OWNER_ROLE",
"ENCLAVE_READER_ROLE",
"EVENTGRID_CONTRIBUTOR",
"EVENTGRID_DATA_CONTRIBUTOR",
"EVENTGRID_DATA_RECEIVER",
"EVENTGRID_DATA_SENDER",
"EVENTGRID_EVENTSUBSCRIPTION_CONTRIBUTOR",
"EVENTGRID_EVENTSUBSCRIPTION_READER",
"EVENTGRID_TOPICSPACES_PUBLISHER",
"EVENTGRID_TOPICSPACES_SUBSCRIBER",
"EXPERIMENTATION_ADMINISTRATOR",
"EXPERIMENTATION_CONTRIBUTOR",
"EXPERIMENTATION_METRIC_CONTRIBUTOR",
"EXPERIMENTATION_READER",
"FHIR_DATA_CONTRIBUTOR",
"FHIR_DATA_CONVERTER",
"FHIR_DATA_EXPORTER",
"FHIR_DATA_IMPORTER",
"FHIR_DATA_READER",
"FHIR_DATA_WRITER",
"FHIR_SMART_USER",
"FIRMWARE_ANALYSIS_ADMIN",
"FLUX_CONFIGURATIONS_CONTRIBUTOR",
"GEOCATALOG_ADMINISTRATOR",
"GEOCATALOG_READER",
"GITOPS_LZ_LIST_ACTIONS",
"GRAFANA_ADMIN",
"GRAFANA_EDITOR",
"GRAFANA_LIMITED_VIEWER",
"GRAFANA_VIEWER",
"GRAPH_OWNER",
"GROUPQUOTA_READER",
"GROUPQUOTA_REQUEST_OPERATOR",
"GUEST_CONFIGURATION_RESOURCE_CONTRIBUTOR",
"HDINSIGHT_CLUSTER_ADMIN",
"HDINSIGHT_CLUSTER_OPERATOR",
"HDINSIGHT_DOMAIN_SERVICES_CONTRIBUTOR",
"HDINSIGHT_ON_AKS_CLUSTER_ADMIN",
"HDINSIGHT_ON_AKS_CLUSTER_OPERATOR",
"HDINSIGHT_ON_AKS_CLUSTER_POOL_ADMIN",
"HEALTHCARE_AGENT_ADMIN",
"HEALTHCARE_AGENT_EDITOR",
"HEALTHCARE_AGENT_READER",
"HEALTH_SAFEGUARDS_DATA_USER",
"HIERARCHY_SETTINGS_ADMINISTRATOR",
"HYBRIDCOMPUTE_MACHINE_LISTACCESSDETAILS_ACTION_IN_BUILT_ROLE",
"HYBRID_SERVER_ONBOARDING",
"HYBRID_SERVER_RESOURCE_ADMINISTRATOR",
"IMPACT_READER",
"IMPACT_REPORTER",
"INTEGRATION_SERVICE_ENVIRONMENT_CONTRIBUTOR",
"INTEGRATION_SERVICE_ENVIRONMENT_DEVELOPER",
"INTELLIGENT_SYSTEMS_ACCOUNT_CONTRIBUTOR",
"IOT_HUB_DATA_CONTRIBUTOR",
"IOT_HUB_DATA_READER",
"IOT_HUB_REGISTRY_CONTRIBUTOR",
"IOT_HUB_TWIN_CONTRIBUTOR",
"IPAM_POOL_USER",
"ISSUE_CONTRIBUTOR",
"KEY_VAULT_ADMINISTRATOR",
"KEY_VAULT_CERTIFICATES_OFFICER",
"KEY_VAULT_CERTIFICATE_USER",
"KEY_VAULT_CONTRIBUTOR",
"KEY_VAULT_CRYPTO_OFFICER",
"KEY_VAULT_CRYPTO_SERVICE_ENCRYPTION_USER",
"KEY_VAULT_CRYPTO_SERVICE_RELEASE_USER",
"KEY_VAULT_CRYPTO_USER",
"KEY_VAULT_DATA_ACCESS_ADMINISTRATOR",
"KEY_VAULT_PURGE_OPERATOR",
"KEY_VAULT_READER",
"KEY_VAULT_SECRETS_OFFICER",
"KEY_VAULT_SECRETS_USER",
"KNOWLEDGE_CONSUMER",
"KOSTNER_DFS_BASE_ROLE",
"KUBERNETESRUNTIME_LOAD_BALANCER_CONTRIBUTOR_ROLE",
"KUBERNETES_AGENTLESS_OPERATOR",
"KUBERNETES_AGENT_OPERATOR",
"KUBERNETES_AGENT_SUBSCRIPTION_LEVEL_OPERATOR",
"KUBERNETES_CLUSTER_AZURE_ARC_ONBOARDING",
"KUBERNETES_EXTENSION_CONTRIBUTOR",
"KUBERNETES_NAMESPACE_USER",
"KUBERNETES_RUNTIME_STORAGE_CLASS_CONTRIBUTOR_ROLE",
"LAB_ASSISTANT",
"LAB_CONTRIBUTOR",
"LAB_CREATOR",
"LAB_OPERATOR",
"LAB_SERVICES_CONTRIBUTOR",
"LAB_SERVICES_READER",
"LANDING_ZONE_ACCOUNT_OWNER",
"LANDING_ZONE_ACCOUNT_READER",
"LANDING_ZONE_MANAGEMENT_OWNER",
"LANDING_ZONE_MANAGEMENT_READER",
"LOAD_TEST_CONTRIBUTOR",
"LOAD_TEST_OWNER",
"LOAD_TEST_READER",
"LOCALNGFIREWALLADMINISTRATOR_ROLE",
"LOCALRULESTACKSADMINISTRATOR_ROLE",
"LOCKS_CONTRIBUTOR",
"LOGIC_APPS_STANDARD_CONTRIBUTOR_PREVIEW",
"LOGIC_APPS_STANDARD_DEVELOPER_PREVIEW",
"LOGIC_APPS_STANDARD_OPERATOR_PREVIEW",
"LOGIC_APPS_STANDARD_READER_PREVIEW",
"LOGIC_APP_CONTRIBUTOR",
"LOGIC_APP_OPERATOR",
"LOG_ANALYTICS_CONTRIBUTOR",
"LOG_ANALYTICS_READER",
"MANAGED_APPLICATION_CONTRIBUTOR_ROLE",
"MANAGED_APPLICATION_OPERATOR_ROLE",
"MANAGED_APPLICATION_PUBLISHER_OPERATOR",
"MANAGED_HSM_CONTRIBUTOR",
"MANAGED_IDENTITY_CONTRIBUTOR",
"MANAGED_IDENTITY_FEDERATED_IDENTITY_CREDENTIAL_CONTRIBUTOR",
"MANAGED_IDENTITY_OPERATOR",
"MANAGED_SERVICES_REGISTRATION_ASSIGNMENT_DELETE_ROLE",
"MANAGEMENT_GROUP_CONTRIBUTOR",
"MANAGEMENT_GROUP_READER",
"MEDIA_SERVICES_ACCOUNT_ADMINISTRATOR",
"MEDIA_SERVICES_LIVE_EVENTS_ADMINISTRATOR",
"MEDIA_SERVICES_MEDIA_OPERATOR",
"MEDIA_SERVICES_POLICY_ADMINISTRATOR",
"MEDIA_SERVICES_STREAMING_ENDPOINTS_ADMINISTRATOR",
"MICROSOFT_EDGE_WINFIELDS_FEDERATED_SUBSCRIPTION_READ_ACCESS_ROLE",
"MICROSOFT_KUBERNETES_CONNECTED_CLUSTER_ROLE",
"MICROSOFT_POWERBI_TENANT_OPERATIONS_ROLE",
"MICROSOFT_SENTINEL_AUTOMATION_CONTRIBUTOR",
"MICROSOFT_SENTINEL_BUSINESS_APPLICATIONS_AGENT_OPERATOR",
"MICROSOFT_SENTINEL_CONTRIBUTOR",
"MICROSOFT_SENTINEL_PLAYBOOK_OPERATOR",
"MICROSOFT_SENTINEL_READER",
"MICROSOFT_SENTINEL_RESPONDER",
"MONITORING_CONTRIBUTOR",
"MONITORING_DATA_READER",
"MONITORING_METRICS_PUBLISHER",
"MONITORING_READER",
"MYSQL_BACKUP_AND_EXPORT_OPERATOR",
"NETWORK_CONTRIBUTOR",
"NEW_RELIC_APM_ACCOUNT_CONTRIBUTOR",
"NEXUS_NETWORK_FABRIC_SERVICE_READER",
"NEXUS_NETWORK_FABRIC_SERVICE_WRITER",
"OBJECT_ANCHORS_ACCOUNT_OWNER",
"OBJECT_ANCHORS_ACCOUNT_READER",
"OBJECT_UNDERSTANDING_ACCOUNT_OWNER",
"OBJECT_UNDERSTANDING_ACCOUNT_READER",
"ONLINE_EXPERIMENTATION_CONTRIBUTOR",
"ONLINE_EXPERIMENTATION_DATA_OWNER",
"ONLINE_EXPERIMENTATION_DATA_READER",
"ONLINE_EXPERIMENTATION_READER",
"OPENCOST_RATE_CARD_READER",
"OPERATOR_NEXUS_COMPUTE_CONTRIBUTOR_ROLE_PREVIEW",
"OPERATOR_NEXUS_KEY_VAULT_WRITER_SERVICE_ROLE_PREVIEW",
"OPERATOR_NEXUS_OWNER_PREVIEW",
"ORACLE_DATABASE_AUTONOMOUS_DATABASE_ADMINISTRATOR",
"ORACLE_DATABASE_EXADATA_INFRASTRUCTURE_ADMINISTRATOR_BUILT_IN_ROLE",
"ORACLE_DATABASE_EXASCALE_STORAGE_VAULT_ADMINISTRATOR",
"ORACLE_DATABASE_EXASCALE_VMCLUSTER_ADMINISTRATOR",
"ORACLE_DATABASE_OWNER_BUILT_IN_ROLE",
"ORACLE_DATABASE_READER_BUILT_IN_ROLE",
"ORACLE_DATABASE_VMCLUSTER_ADMINISTRATOR_BUILT_IN_ROLE",
"ORACLE_SUBSCRIPTIONS_MANAGER_BUILT_IN_ROLE",
"OWNER",
"PLAYFAB_CONTRIBUTOR",
"PLAYFAB_READER",
"POLICY_INSIGHTS_DATA_WRITER_PREVIEW",
"PORTAL_DASHBOARD_WRITER_SERVICE_ROLE",
"POSTGRESQL_FLEXIBLE_SERVER_LONG_TERM_RETENTION_BACKUP_ROLE",
"POWER_PLATFORM_ACCOUNT_CONTRIBUTOR",
"POWER_PLATFORM_ENTERPRISE_POLICY_CONTRIBUTOR",
"PRIVATE_DNS_ZONE_CONTRIBUTOR",
"PROCUREMENT_CONTRIBUTOR",
"PROJECT_BABYLON_DATA_CURATOR",
"PROJECT_BABYLON_DATA_READER",
"PROJECT_BABYLON_DATA_SOURCE_ADMINISTRATOR",
"PROVIDERHUB_CONTRIBUTOR",
"PROVIDERHUB_READER",
"QUANTUM_WORKSPACE_DATA_CONTRIBUTOR",
"QUOTA_REQUEST_OPERATOR",
"READER",
"READER_AND_DATA_ACCESS",
"REDIS_CACHE_CONTRIBUTOR",
"REMOTE_RENDERING_ADMINISTRATOR",
"REMOTE_RENDERING_CLIENT",
"RESERVATION_PURCHASER",
"RESOURCE_POLICY_CONTRIBUTOR",
"ROLE_BASED_ACCESS_CONTROL_ADMINISTRATOR",
"SAAS_HUB_CONTRIBUTOR",
"SAVINGS_PLAN_PURCHASER",
"SCHEDULED_EVENTS_CONTRIBUTOR",
"SCHEDULED_PATCHING_CONTRIBUTOR",
"SCHEDULER_JOB_COLLECTIONS_CONTRIBUTOR",
"SCHEMA_REGISTRY_CONTRIBUTOR",
"SCHEMA_REGISTRY_READER",
"SEARCH_INDEX_DATA_CONTRIBUTOR",
"SEARCH_INDEX_DATA_READER",
"SEARCH_PARAMETER_MANAGER",
"SEARCH_SERVICE_CONTRIBUTOR",
"SECRETS_STORE_EXTENSION_OWNER",
"SECURE_SCORE_READER",
"SECURITY_ADMIN",
"SECURITY_ASSESSMENT_CONTRIBUTOR",
"SECURITY_DETONATION_CHAMBER_PUBLISHER",
"SECURITY_DETONATION_CHAMBER_READER",
"SECURITY_DETONATION_CHAMBER_SUBMISSION_MANAGER",
"SECURITY_DETONATION_CHAMBER_SUBMITTER",
"SECURITY_MANAGER_LEGACY",
"SECURITY_READER",
"SERVICES_HUB_OPERATOR",
"SERVICE_CONNECTOR_CONTRIBUTOR",
"SERVICE_FABRIC_CLUSTER_CONTRIBUTOR",
"SERVICE_FABRIC_MANAGED_CLUSTER_CONTRIBUTOR",
"SERVICE_GROUP_READER",
"SIGNALR_ACCESSKEY_READER",
"SIGNALR_APP_SERVER",
"SIGNALR_REST_API_OWNER",
"SIGNALR_REST_API_READER",
"SIGNALR_SERVICE_OWNER",
"SIGNALR_WEB_PUBSUB_CONTRIBUTOR",
"SITE_RECOVERY_CONTRIBUTOR",
"SITE_RECOVERY_OPERATOR",
"SITE_RECOVERY_READER",
"SPATIALMAPSACCOUNTS_ACCOUNT_OWNER",
"SPATIAL_ANCHORS_ACCOUNT_CONTRIBUTOR",
"SPATIAL_ANCHORS_ACCOUNT_OWNER",
"SPATIAL_ANCHORS_ACCOUNT_READER",
"SQLDB_MIGRATION_ROLE",
"SQLMI_MIGRATION_ROLE",
"SQLVM_MIGRATION_ROLE",
"SQL_DB_CONTRIBUTOR",
"SQL_MANAGED_INSTANCE_CONTRIBUTOR",
"SQL_SECURITY_MANAGER",
"SQL_SERVER_CONTRIBUTOR",
"SSH_PUBLICKEYS_CONTRIBUTOR_ROLE",
"SSH_PUBLICKEYS_READER_ROLE",
"STANDBY_CONTAINER_GROUP_POOL_CONTRIBUTOR",
"STORAGE_ACCOUNT_BACKUP_CONTRIBUTOR",
"STORAGE_ACCOUNT_CONTRIBUTOR",
"STORAGE_ACCOUNT_ENCRYPTION_SCOPE_CONTRIBUTOR_ROLE",
"STORAGE_ACCOUNT_KEY_OPERATOR_SERVICE_ROLE",
"STORAGE_BLOB_DATA_CONTRIBUTOR",
"STORAGE_BLOB_DATA_OWNER",
"STORAGE_BLOB_DATA_READER",
"STORAGE_BLOB_DELEGATOR",
"STORAGE_FILE_DATA_PRIVILEGED_CONTRIBUTOR",
"STORAGE_FILE_DATA_PRIVILEGED_READER",
"STORAGE_FILE_DATA_SMB_SHARE_CONTRIBUTOR",
"STORAGE_FILE_DATA_SMB_SHARE_ELEVATED_CONTRIBUTOR",
"STORAGE_FILE_DATA_SMB_SHARE_READER",
"STORAGE_QUEUE_DATA_CONTRIBUTOR",
"STORAGE_QUEUE_DATA_MESSAGE_PROCESSOR",
"STORAGE_QUEUE_DATA_MESSAGE_SENDER",
"STORAGE_QUEUE_DATA_READER",
"STORAGE_TABLE_DATA_CONTRIBUTOR",
"STORAGE_TABLE_DATA_READER",
"STREAM_ANALYTICS_CONTRIBUTOR",
"STREAM_ANALYTICS_QUERY_TESTER",
"STREAM_ANALYTICS_READER",
"SUBSCRIPTION_CREATOR",
"SUBSCRIPTION_CREATOR_OLD",
"SUPPORT_REQUEST_CONTRIBUTOR",
"TAG_CONTRIBUTOR",
"TAG_READER",
"TEMPLATE_SPEC_CONTRIBUTOR",
"TEMPLATE_SPEC_READER",
"TEST_BASE_READER",
"TOOLCHAINORCHESTRATOR_ADMIN_ROLE",
"TOOLCHAINORCHESTRATOR_VIEWER_ROLE",
"TRAFFIC_MANAGER_CONTRIBUTOR",
"TRANSPARENCY_LOGS_OWNER",
"TRUSTED_SIGNING_CERTIFICATE_PROFILE_SIGNER",
"TRUSTED_SIGNING_IDENTITY_VERIFIER",
"USAGE_BILLING_CONTRIBUTOR",
"USER_ACCESS_ADMINISTRATOR",
"VIDEO_INDEXER_RESTRICTED_VIEWER",
"VIRTUAL_MACHINE_ADMINISTRATOR_LOGIN",
"VIRTUAL_MACHINE_CONTRIBUTOR",
"VIRTUAL_MACHINE_DATA_ACCESS_ADMINISTRATOR_PREVIEW",
"VIRTUAL_MACHINE_LOCAL_USER_LOGIN",
"VIRTUAL_MACHINE_USER_LOGIN",
"VM_RESTORE_OPERATOR",
"VM_SCANNER_OPERATOR",
"WEBSITE_CONTRIBUTOR",
"WEB_PLAN_CONTRIBUTOR",
"WEB_PUBSUB_SERVICE_OWNER",
"WEB_PUBSUB_SERVICE_READER",
"WINDOWS_ADMIN_CENTER_ADMINISTRATOR_LOGIN",
"WORKBOOK_CONTRIBUTOR",
"WORKBOOK_READER",
"WORKLOADBUILDER_MIGRATION_AGENT_ROLE",
"WORKLOAD_ORCHESTRATION_IT_ADMIN",
"WORKLOAD_ORCHESTRATION_SOLUTION_EXTERNAL_VALIDATOR"
]

View File

@@ -1,249 +0,0 @@
"""
Curated signature database for Bicep's built-in functions.
Bicep.LangServer (as of 0.46.1) does not implement textDocument/signatureHelp,
so iLSP provides its own for the most commonly used built-in functions.
Not exhaustive — covers the functions people actually type day to day.
"""
from typing import Any
# name -> list of parameter labels (one entry per overload, simplest form first)
BUILTIN_FUNCTIONS: dict[str, dict[str, Any]] = {
"resourceId": {
"params": ["resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the unique identifier of a resource. Also usable with a subscriptionId/resourceGroupName prefix.",
},
"subscriptionResourceId": {
"params": ["subscriptionId", "resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the unique identifier for a resource deployed at the subscription level.",
},
"tenantResourceId": {
"params": ["resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the unique identifier for a resource deployed at the tenant level.",
},
"extensionResourceId": {
"params": ["resourceId", "resourceType", "resourceName1", "resourceName2..."],
"doc": "Returns the resource ID for an extension resource, which is applied to another resource.",
},
"guid": {
"params": ["baseString1", "baseString2..."],
"doc": "Creates a deterministic GUID based on the values passed as parameters.",
},
"uniqueString": {
"params": ["baseString1", "baseString2..."],
"doc": "Creates a deterministic hash string based on the values passed as parameters (13 chars).",
},
"take": {
"params": ["originalValue", "count"],
"doc": "Returns a string or array with the specified number of elements from the start.",
},
"skip": {
"params": ["originalValue", "count"],
"doc": "Returns a string or array with all the elements after the specified number skipped.",
},
"first": {
"params": ["arg1"],
"doc": "Returns the first element of an array, or first character of a string.",
},
"last": {
"params": ["arg1"],
"doc": "Returns the last element of an array, or last character of a string.",
},
"length": {
"params": ["arg1"],
"doc": "Returns the number of elements in an array, characters in a string, or properties in an object.",
},
"concat": {
"params": ["arg1", "arg2..."],
"doc": "Combines multiple arrays or strings.",
},
"union": {
"params": ["arg1", "arg2..."],
"doc": "Returns a single array/object with all elements from the parameters, merging objects.",
},
"contains": {
"params": ["container", "itemToFind"],
"doc": "Checks whether an array contains a value, an object contains a key, or a string contains a substring.",
},
"empty": {
"params": ["itemToTest"],
"doc": "Determines if an array, object, or string is empty.",
},
"format": {
"params": ["formatString", "arg1..."],
"doc": "Creates a formatted string using .NET-style {0} placeholders.",
},
"join": {
"params": ["inputArray", "delimiter"],
"doc": "Joins multiple strings from an array into a single string, separated by delimiter.",
},
"split": {
"params": ["inputString", "delimiter"],
"doc": "Splits a string into an array using a delimiter.",
},
"replace": {
"params": ["originalString", "oldString", "newString"],
"doc": "Returns a new string with all instances of oldString replaced by newString.",
},
"toLower": {
"params": ["stringToChange"],
"doc": "Converts a string to lowercase.",
},
"toUpper": {
"params": ["stringToChange"],
"doc": "Converts a string to uppercase.",
},
"trim": {
"params": ["stringToTrim"],
"doc": "Removes leading/trailing whitespace from a string.",
},
"substring": {
"params": ["stringToParse", "startIndex", "length"],
"doc": "Returns a substring starting at the specified position.",
},
"padLeft": {
"params": ["valueToPad", "totalLength", "paddingCharacter"],
"doc": "Pads a string to a fixed total length by adding characters to the left.",
},
"startsWith": {
"params": ["stringToSearch", "stringToFind"],
"doc": "Checks whether a string starts with a value (case-insensitive).",
},
"endsWith": {
"params": ["stringToSearch", "stringToFind"],
"doc": "Checks whether a string ends with a value (case-insensitive).",
},
"indexOf": {
"params": ["stringToSearch", "stringToFind"],
"doc": "Returns the first position of a value within a string (case-insensitive).",
},
"reference": {
"params": ["resourceNameOrIdentifier", "apiVersion", "'Full'"],
"doc": "Returns an object representing a resource's runtime state.",
},
"listKeys": {
"params": ["resourceNameOrIdentifier", "apiVersion"],
"doc": "Returns the keys for a resource (e.g. storage account access keys).",
},
"listSecrets": {
"params": ["resourceNameOrIdentifier", "apiVersion"],
"doc": "Returns the secrets for a resource.",
},
"resourceGroup": {
"params": [],
"doc": "Returns an object representing the current resource group.",
},
"subscription": {
"params": [],
"doc": "Returns an object representing the current subscription.",
},
"deployment": {
"params": [],
"doc": "Returns an object representing the current deployment operation.",
},
"environment": {
"params": [],
"doc": "Returns an object describing the Azure cloud environment.",
},
"az": {
"params": [],
"doc": "Namespace for Azure Bicep built-in functions.",
},
"json": {
"params": ["jsonString"],
"doc": "Converts a valid JSON string into a JSON object / array.",
},
"string": {
"params": ["valueToConvert"],
"doc": "Converts the specified value to a string.",
},
"int": {
"params": ["valueToConvert"],
"doc": "Converts the specified value to an integer.",
},
"bool": {
"params": ["value"],
"doc": "Converts the specified value to a boolean.",
},
"array": {
"params": ["valueToConvert"],
"doc": "Converts the specified value to an array.",
},
"base64": {
"params": ["inputString"],
"doc": "Returns the base64 representation of the input string.",
},
"base64ToString": {
"params": ["base64Value"],
"doc": "Converts a base64 representation to a string.",
},
"dateTimeAdd": {
"params": ["base", "duration", "format"],
"doc": "Adds a time duration to a base UTC datetime value.",
},
"utcNow": {
"params": ["format"],
"doc": "Returns the current UTC datetime in the specified format.",
},
"loadTextContent": {
"params": ["filePath", "encoding"],
"doc": "Loads the content of the specified file as a string at compile time.",
},
"loadJsonContent": {
"params": ["filePath", "jsonPath", "encoding"],
"doc": "Loads the content of the specified JSON file at compile time.",
},
"loadFileAsBase64": {
"params": ["filePath"],
"doc": "Loads the specified file as a base64 string at compile time.",
},
"managementGroup": {
"params": ["name"],
"doc": "Returns an object representing the current or specified management group.",
},
"newGuid": {
"params": [],
"doc": "Returns a new (random, non-deterministic) GUID. Only valid for param defaults.",
},
"filter": {
"params": ["array", "lambdaExpression"],
"doc": "Filters an array using a custom filtering lambda expression.",
},
"map": {
"params": ["array", "lambdaExpression"],
"doc": "Applies a custom mapping lambda expression to each element of an array.",
},
"reduce": {
"params": ["array", "initialValue", "lambdaExpression"],
"doc": "Reduces an array with a custom lambda expression.",
},
"sort": {
"params": ["array", "lambdaExpression"],
"doc": "Sorts an array with a custom lambda expression comparator.",
},
"min": {
"params": ["arg1", "arg2..."],
"doc": "Returns the minimum value from an array of integers, or a comma-separated list.",
},
"max": {
"params": ["arg1", "arg2..."],
"doc": "Returns the maximum value from an array of integers, or a comma-separated list.",
},
"range": {
"params": ["startIndex", "count"],
"doc": "Creates an array of integers from startIndex to startIndex+count-1.",
},
"items": {
"params": ["object"],
"doc": "Returns an array of key/value pair objects that represent the properties of an object.",
},
"pickZones": {
"params": ["providerNamespace", "resourceType", "location", "numberOfZones", "offset"],
"doc": "Determines which availability zones are recommended for a resource type/location.",
},
}
def get_signature(name: str) -> dict[str, Any] | None:
return BUILTIN_FUNCTIONS.get(name)

View File

@@ -1,177 +0,0 @@
"""
Local (non-forwarded) implementations of LSP features that Bicep.LangServer
(as of 0.46.1) does not implement itself: textDocument/signatureHelp,
textDocument/foldingRange, and workspace/symbol.
These are intentionally simple, regex/bracket-based implementations — good
enough for real editor usage, not a full Bicep parser. They run entirely in
the iLSP proxy and never touch the underlying Bicep LS process.
"""
from __future__ import annotations
import re
from typing import Any
from .bicep_functions import get_signature
_DECL_RE = re.compile(
r"^\s*(resource|module|var|param|output|func|type)\s+([A-Za-z_][\w]*)",
)
_SYMBOL_KIND = {
"resource": 6, # Method-ish; LSP doesn't have a great "resource" kind, use Variable-family
"module": 2, # Module
"var": 13, # Variable
"param": 13, # Variable
"output": 13, # Variable
"func": 12, # Function
"type": 5, # Class
}
# ── signatureHelp ───────────────────────────────────────────────────────────
def _find_enclosing_call(current: str) -> tuple[str, int] | None:
"""
Walk backwards from the cursor to find the nearest unmatched '(' and the
identifier immediately preceding it, returning (function_name, arg_index).
Returns None if the cursor isn't inside a recognizable function call.
"""
depth = 0
comma_count = 0
i = len(current) - 1
in_string = False
while i >= 0:
ch = current[i]
if ch == "'" and (i == 0 or current[i - 1] != "\\"):
in_string = not in_string
elif not in_string:
if ch in ")]}":
depth += 1
elif ch in "(":
if depth == 0:
# Found the opening paren for the innermost call
name_m = re.search(r"([A-Za-z_][\w]*)\s*$", current[:i])
if name_m:
return name_m.group(1), comma_count
return None
depth -= 1
elif ch in "[{":
depth -= 1
elif ch == "," and depth == 0:
comma_count += 1
i -= 1
return None
def compute_signature_help(lines: list[str], line: int, character: int) -> dict[str, Any] | None:
if line >= len(lines):
return None
current = lines[line][:character]
found = _find_enclosing_call(current)
if not found:
return None
func_name, arg_index = found
sig = get_signature(func_name)
if not sig:
return None
params = sig["params"]
parameters = [{"label": p} for p in params] if params else []
active_param = min(arg_index, max(len(parameters) - 1, 0)) if parameters else 0
label = f"{func_name}({', '.join(params)})"
return {
"signatures": [
{
"label": label,
"documentation": {"kind": "markdown", "value": sig["doc"]},
"parameters": parameters,
}
],
"activeSignature": 0,
"activeParameter": active_param,
}
# ── foldingRange ────────────────────────────────────────────────────────────
def compute_folding_ranges(lines: list[str]) -> list[dict[str, Any]]:
"""
Simple stack-based bracket matcher over {}, [], () — collapses any
multi-line bracket pair into a folding range. Also folds contiguous
leading '//' comment blocks.
"""
ranges: list[dict[str, Any]] = []
stack: list[tuple[str, int]] = []
in_string = False
for lineno, text in enumerate(lines):
i = 0
while i < len(text):
ch = text[i]
if ch == "'" and (i == 0 or text[i - 1] != "\\"):
in_string = not in_string
elif not in_string:
if ch in "{[(":
stack.append((ch, lineno))
elif ch in "}])":
if stack:
_open_ch, open_line = stack.pop()
if lineno > open_line:
ranges.append({"startLine": open_line, "endLine": lineno, "kind": "region"})
i += 1
# Fold contiguous line-comment blocks (3+ lines)
i = 0
while i < len(lines):
if lines[i].strip().startswith("//"):
start = i
while i < len(lines) and lines[i].strip().startswith("//"):
i += 1
if i - start >= 3:
ranges.append({"startLine": start, "endLine": i - 1, "kind": "comment"})
else:
i += 1
ranges.sort(key=lambda r: (r["startLine"], r["endLine"]))
return ranges
# ── workspace/symbol ────────────────────────────────────────────────────────
def compute_workspace_symbols(docs: dict[str, list[str]], query: str) -> list[dict[str, Any]]:
"""
Best-effort workspace symbol search over documents currently open in this
editor session (iLSP has no full-workspace index — only what the client
has opened). Matches top-level resource/module/var/param/output/func/type
declarations whose name contains the query (case-insensitive).
"""
query_l = query.lower()
results: list[dict[str, Any]] = []
for uri, lines in docs.items():
for lineno, text in enumerate(lines):
m = _DECL_RE.match(text)
if not m:
continue
kind_word, name = m.group(1), m.group(2)
if query_l and query_l not in name.lower():
continue
start_char = text.index(name, m.end(1))
results.append({
"name": name,
"kind": _SYMBOL_KIND.get(kind_word, 13),
"location": {
"uri": uri,
"range": {
"start": {"line": lineno, "character": start_char},
"end": {"line": lineno, "character": start_char + len(name)},
},
},
"containerName": kind_word,
})
return results

View File

@@ -18,20 +18,8 @@ from typing import Any
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Known Azure enum values not always captured in the catalog schema # Known Azure enum values not always captured in the catalog schema
def _load_azure_roles() -> list[str]:
"""Load Azure roles from azure_roles.json (generated from bicep/lookup/rbaclookup)."""
roles_file = pathlib.Path(__file__).parent / "azure_roles.json"
if roles_file.exists():
try:
return json.loads(roles_file.read_text())
except Exception as e:
logger.warning("Failed to load azure_roles.json: %s", e)
return []
_KNOWN_ENUMS: dict[str, list[str]] = { _KNOWN_ENUMS: dict[str, list[str]] = {
"principalType": ["User", "Group", "ServicePrincipal", "Device", "ForeignGroup"], "principalType": ["User", "Group", "ServicePrincipal", "Device", "ForeignGroup"],
"roles": _load_azure_roles(),
"roleDefinitionIds": _load_azure_roles(), # alias for roles
} }
# Catalog is baked into the image root at /bicep_modules_catalog.json # Catalog is baked into the image root at /bicep_modules_catalog.json
@@ -48,6 +36,34 @@ _IAC_SOURCE_PATHS = [
pathlib.Path(__file__).parent.parent.parent / "iac_source_catalog.json", # dev pathlib.Path(__file__).parent.parent.parent / "iac_source_catalog.json", # dev
] ]
# Principals catalog — known object IDs for array params (e.g. additionalAccess)
# Format: {"params": {"additionalAccess": [{"id": "<guid>", "label": "...", "description": "..."}]}}
_PRINCIPALS_CATALOG_PATHS = [
pathlib.Path("/data/principals_catalog.json"), # volume-mount (freshest)
pathlib.Path("/principals_catalog.json"), # baked into image (fallback)
pathlib.Path(__file__).parent.parent.parent / "principals_catalog.json", # dev
]
def _load_principals_catalog() -> dict[str, list[dict[str, Any]]]:
"""Load principals catalog for array param completions.
Returns dict keyed by param name (e.g. 'additionalAccess') → list of
{id, label, description} entries.
"""
for path in _PRINCIPALS_CATALOG_PATHS:
if path.exists():
try:
data = json.loads(path.read_text())
params = data.get("params", {})
count = sum(len(v) for v in params.values())
logger.info("Principals catalog loaded from %s: %d entries", path, count)
return params
except Exception:
logger.exception("Failed to parse principals catalog at %s", path)
logger.debug("No principals_catalog.json found — array param completions unavailable")
return {}
def _load_iac_source_catalog() -> dict[str, dict[str, Any]]: def _load_iac_source_catalog() -> dict[str, dict[str, Any]]:
"""Load IAC source catalog for enriched param descriptions. """Load IAC source catalog for enriched param descriptions.
@@ -106,12 +122,14 @@ class BicepModuleCatalog:
_modules: list[dict[str, Any]] = [] _modules: list[dict[str, Any]] = []
_iac: dict[str, dict[str, Any]] = {} # module name → IAC source info _iac: dict[str, dict[str, Any]] = {} # module name → IAC source info
_principals: dict[str, list[dict[str, Any]]] = {} # param name → [{id, label, description}]
@classmethod @classmethod
def load(cls) -> None: def load(cls) -> None:
"""Load both catalogs from disk. Call once at startup.""" """Load all catalogs from disk. Call once at startup."""
cls._modules = _load_catalog() cls._modules = _load_catalog()
cls._iac = _load_iac_source_catalog() cls._iac = _load_iac_source_catalog()
cls._principals = _load_principals_catalog()
@classmethod @classmethod
def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]: def _iac_param_map(cls, module_name: str) -> dict[str, dict[str, Any]]:
@@ -258,6 +276,44 @@ class BicepModuleCatalog:
}) })
return items return items
@classmethod
def param_array_item_completion_items(
cls,
module_name: str,
version: str,
param_name: str,
has_open_quote: bool = False,
) -> list[dict[str, Any]]:
"""Completions for items inside an array param (e.g. additionalAccess objectIds).
Looks up known entries from the principals catalog keyed by param name.
"""
entries = cls._principals.get(param_name, [])
if not entries:
return []
items = []
for i, entry in enumerate(entries):
val = entry["id"]
label = entry.get("label", val)
description = entry.get("description", "")
insert = f"{val}'" if has_open_quote else f"'{val}'"
doc = f"**{label}**\n\n`{val}`"
if description:
doc += f"\n\n{description}"
items.append({
"label": label,
"kind": 12, # Value
"detail": val, # GUID shown as detail
"insertText": insert,
"sortText": f"0_lru_arr_{i:03d}_{label}",
"documentation": {
"kind": "markdown",
"value": doc,
},
})
return items
@classmethod @classmethod
def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]: def param_completion_items(cls, module_name: str, version: str) -> list[dict[str, Any]]:
"""Param completions for a specific module+version combination.""" """Param completions for a specific module+version combination."""
@@ -280,56 +336,6 @@ class BicepModuleCatalog:
iac_params = cls._iac_param_map(module_name) iac_params = cls._iac_param_map(module_name)
items = [] items = []
# Build snippet for full params block (shown first)
snippet_params = []
tabstop = 1
for param_name, param_info in ver_params.items():
iac = iac_params.get(param_name, {})
required = iac.get("required", False)
ptype = param_info.get("type", "any")
allowed = param_info.get("allowed", [])
# Include required params + first few optional params in snippet
if required or len(snippet_params) < 5:
if allowed:
# Enum: use placeholder with first allowed value
placeholder = f"'{allowed[0]}'"
elif ptype == "bool":
placeholder = "true"
elif ptype == "int":
placeholder = "0"
elif ptype == "array":
placeholder = "[]"
elif ptype == "object":
placeholder = "{{}}"
else:
placeholder = "''"
snippet_params.append(f" {param_name}: ${{{tabstop}:{placeholder}}}")
tabstop += 1
if snippet_params:
snippet_text = "\n" + "\n".join(snippet_params) + "\n"
required_count = sum(1 for p, i in ver_params.items()
if iac_params.get(p, {}).get("required", False))
items.append({
"label": "⚡ Fill params block",
"kind": 15, # Snippet
"detail": f"{len(snippet_params)} params ({required_count} required)",
"insertText": snippet_text,
"insertTextFormat": 2, # Snippet
"sortText": "0_lru_snippet_000",
"documentation": {
"kind": "markdown",
"value": (
f"**Fill params block**\n\n"
f"Inserts {len(snippet_params)} params for `{module_name}`.\n"
f"Use Tab to navigate between fields."
),
},
})
# Individual param completions
for param_name, param_info in ver_params.items(): for param_name, param_info in ver_params.items():
ptype = param_info.get("type", "any") ptype = param_info.get("type", "any")
allowed = param_info.get("allowed", []) allowed = param_info.get("allowed", [])

View File

@@ -27,11 +27,6 @@ import subprocess
import threading import threading
from typing import Any from typing import Any
from .local_features import (
compute_folding_ranges,
compute_signature_help,
compute_workspace_symbols,
)
from .modules import BicepModuleCatalog from .modules import BicepModuleCatalog
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -135,6 +130,19 @@ class _ProxySession:
mod_name = last_mod.group(1) mod_name = last_mod.group(1)
mod_ver = last_mod.group(2) mod_ver = last_mod.group(2)
# Array item context: cursor inside [...] for an array param.
# Must be checked BEFORE value_m, since array lines also match
# the value pattern (e.g. `additionalAccess: ['`).
array_m = re.search(r"^\s*(\w+):\s*\[", current)
if array_m and current.count("[") > current.count("]"):
return {
"type": "param_array_item",
"module": mod_name,
"version": mod_ver,
"param": array_m.group(1),
"has_open_quote": bool(re.search(r"'[^']*$", current)),
}
# Check if cursor is after 'paramname: ' on the current line # Check if cursor is after 'paramname: ' on the current line
# (value context — inject enum/allowed values) # (value context — inject enum/allowed values)
value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current) value_m = re.search(r"^\s*(\w+):\s*('?)([^'{}]*)$", current)
@@ -147,43 +155,6 @@ class _ProxySession:
"has_open_quote": bool(value_m.group(2)), "has_open_quote": bool(value_m.group(2)),
} }
# Check if cursor is inside an array value for a param
# e.g. "roles: ['KEY_VAULT_" or "roles: [ '"
array_m = re.search(r"^\s*(\w+):\s*\[[^\]]*?('?)([^',\]]*)$", current)
if array_m and array_m.group(1) not in {"params", "name", "module", "resource"}:
return {
"type": "param_value",
"module": mod_name,
"version": mod_ver,
"param": array_m.group(1),
"has_open_quote": bool(array_m.group(2)),
}
# Check if cursor is inside a multi-line array element
# e.g. " 'APP_CONFIG" on line after "roles: ["
# Walk backwards to find the array opening
for lookback_idx in range(line_idx - 1, max(0, line_idx - 10), -1):
prev_line = lines[lookback_idx]
# Found array opening like "roles: [" or " roles: ["
array_open_m = re.match(r"^\s*(\w+):\s*\[$", prev_line.rstrip())
if array_open_m:
param_name = array_open_m.group(1)
if param_name not in {"params", "name", "module", "resource"}:
# Check if current line is inside the array (has quote or is indented)
if re.match(r"^\s+('?)([^',\]]*)\s*$", current):
has_quote = bool(re.match(r"^\s+'", current))
return {
"type": "param_value",
"module": mod_name,
"version": mod_ver,
"param": param_name,
"has_open_quote": has_quote,
}
break
# Stop if we hit a closing bracket (we're outside the array)
if "]" in prev_line:
break
return { return {
"type": "param", "type": "param",
"module": mod_name, "module": mod_name,
@@ -192,12 +163,8 @@ class _ProxySession:
return {"type": "unknown"} return {"type": "unknown"}
def pop_context(self, msg_id) -> dict | None: def pop_context(self, msg_id) -> dict:
"""Return the tracked completion context for msg_id, or None if this return self.pending.pop(msg_id, {"type": "unknown"})
response id does not correspond to a textDocument/completion request
we recorded (e.g. hover/definition/references/... responses must never
be treated as completion responses)."""
return self.pending.pop(msg_id, None)
# ── Completion injection ─────────────────────────────────────────────────────── # ── Completion injection ───────────────────────────────────────────────────────
@@ -243,11 +210,18 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
context["param"], context["param"],
context.get("has_open_quote", False), context.get("has_open_quote", False),
) )
elif ctx_type == "param_array_item":
lru_items = BicepModuleCatalog.param_array_item_completion_items(
context["module"],
context["version"],
context["param"],
context.get("has_open_quote", False),
)
else: else:
# Default: module name completions # Default: module name completions
lru_items = BicepModuleCatalog.as_completion_items() lru_items = BicepModuleCatalog.as_completion_items()
if ctx_type in ("version", "param", "param_value"): if ctx_type in ("version", "param", "param_value", "param_array_item"):
# Always replace LS completions for private-registry contexts — the # Always replace LS completions for private-registry contexts — the
# Bicep LS doesn't know about our ACR, so anything it returns is noise. # Bicep LS doesn't know about our ACR, so anything it returns is noise.
# Even if lru_items is empty (no enum values for a param), suppress LS. # Even if lru_items is empty (no enum values for a param), suppress LS.
@@ -270,26 +244,8 @@ def _inject_completions(msg: dict[str, Any], context: dict | None = None) -> byt
return json.dumps(msg).encode() return json.dumps(msg).encode()
def _local_response(msg_id, result: Any) -> bytes:
return json.dumps({"jsonrpc": "2.0", "id": msg_id, "result": result}).encode()
class _LockedConn:
"""Wraps a socket so concurrent writers (locally-answered responses vs.
forwarded LS responses) never interleave partial frames on the wire."""
def __init__(self, conn: socket.socket, lock: threading.Lock) -> None:
self._conn = conn
self._lock = lock
def sendall(self, data: bytes) -> None:
with self._lock:
self._conn.sendall(data)
def _client_to_ls( def _client_to_ls(
conn_file, conn_file,
conn: socket.socket,
proc_stdin, proc_stdin,
session: _ProxySession, session: _ProxySession,
) -> None: ) -> None:
@@ -299,7 +255,6 @@ def _client_to_ls(
logger.debug("Client→LS: %d bytes", len(body)) logger.debug("Client→LS: %d bytes", len(body))
# Track document state and completion context (never block forwarding) # Track document state and completion context (never block forwarding)
forward = True
try: try:
msg = json.loads(body) msg = json.loads(body)
method = msg.get("method", "") method = msg.get("method", "")
@@ -315,33 +270,9 @@ def _client_to_ls(
session.update_doc(uri, changes[-1].get("text", "")) session.update_doc(uri, changes[-1].get("text", ""))
elif method == "textDocument/completion": elif method == "textDocument/completion":
session.record_completion_request(msg) session.record_completion_request(msg)
elif method == "textDocument/signatureHelp":
# Bicep.LangServer doesn't implement this — answer locally.
forward = False
params = msg.get("params", {})
uri = params.get("textDocument", {}).get("uri", "")
pos = params.get("position", {})
lines = session.docs.get(uri, [])
result = compute_signature_help(lines, pos.get("line", 0), pos.get("character", 0))
conn.sendall(_frame(_local_response(msg.get("id"), result)))
elif method == "textDocument/foldingRange":
# Bicep.LangServer doesn't implement this — answer locally.
forward = False
uri = msg.get("params", {}).get("textDocument", {}).get("uri", "")
lines = session.docs.get(uri, [])
result = compute_folding_ranges(lines)
conn.sendall(_frame(_local_response(msg.get("id"), result)))
elif method == "workspace/symbol":
# Bicep.LangServer doesn't implement this — answer locally,
# scoped to documents open in this editor session.
forward = False
query = msg.get("params", {}).get("query", "")
result = compute_workspace_symbols(session.docs, query)
conn.sendall(_frame(_local_response(msg.get("id"), result)))
except Exception: except Exception:
forward = True # parsing/handling errors must never block forwarding pass # parsing errors must never block forwarding
if forward:
framed = _frame(body) framed = _frame(body)
proc_stdin.write(framed) proc_stdin.write(framed)
proc_stdin.flush() proc_stdin.flush()
@@ -368,26 +299,10 @@ def _ls_to_client(
logger.debug("LS→Client: %d bytes", len(body)) logger.debug("LS→Client: %d bytes", len(body))
try: try:
msg = json.loads(body) msg = json.loads(body)
context = None context: dict = {}
if "id" in msg and "result" in msg: if "id" in msg and "result" in msg:
context = session.pop_context(msg["id"]) context = session.pop_context(msg["id"])
# Only rewrite responses that actually correspond to a
# textDocument/completion request we tracked — any other
# response (hover, definition, references, documentSymbol,
# formatting, codeAction, ...) must pass through untouched.
if context is not None:
out = _inject_completions(msg, context) out = _inject_completions(msg, context)
elif msg.get("method") is None and isinstance(msg.get("result"), dict) and "capabilities" in msg.get("result", {}):
# This is the initialize response — advertise the extra
# capabilities iLSP answers locally (Bicep.LangServer
# itself doesn't implement these).
caps = msg["result"]["capabilities"]
caps["signatureHelpProvider"] = {"triggerCharacters": ["(", ","]}
caps["foldingRangeProvider"] = True
caps["workspaceSymbolProvider"] = True
out = json.dumps(msg).encode()
else:
out = body
except json.JSONDecodeError: except json.JSONDecodeError:
out = body out = body
conn.sendall(_frame(out)) conn.sendall(_frame(out))
@@ -411,22 +326,16 @@ def _handle_client(conn: socket.socket, addr: tuple) -> None:
# Unbuffered read from the socket — critical for correct LSP framing # Unbuffered read from the socket — critical for correct LSP framing
conn_file = conn.makefile("rb", buffering=0) conn_file = conn.makefile("rb", buffering=0)
# t1 may write locally-answered responses (signatureHelp/foldingRange/
# workspace-symbol) to conn concurrently with t2's LS responses — a lock
# prevents interleaved/corrupted frames on the wire.
send_lock = threading.Lock()
locked_conn = _LockedConn(conn, send_lock)
# t1: client → LS (finishes when client closes write side) # t1: client → LS (finishes when client closes write side)
t1 = threading.Thread( t1 = threading.Thread(
target=_client_to_ls, target=_client_to_ls,
args=(conn_file, locked_conn, proc.stdin, session), args=(conn_file, proc.stdin, session),
daemon=True, daemon=True,
) )
# t2: LS → client (finishes when LS closes stdout) # t2: LS → client (finishes when LS closes stdout)
t2 = threading.Thread( t2 = threading.Thread(
target=_ls_to_client, target=_ls_to_client,
args=(proc.stdout, locked_conn, session), args=(proc.stdout, conn, session),
daemon=True, daemon=True,
) )
t1.start() t1.start()

11
principals_catalog.json Normal file
View File

@@ -0,0 +1,11 @@
{
"params": {
"additionalAccess": [
{
"id": "c88bf29d-b13a-4153-9738-8995085a451e",
"label": "LRIADMPRO-IaC-Bicep",
"description": "IaC Bicep pipeline service principal"
}
]
}
}

View File

@@ -24,11 +24,6 @@ ilsp = "ilsp.server:main"
where = ["."] where = ["."]
include = ["ilsp*"] include = ["ilsp*"]
[tool.setuptools.package-data]
"ilsp.bicep_lsp" = ["*.json"]
"ilsp.yaml_lsp" = ["*.json"]
"ilsp.python_lsp" = ["*.json"]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"pytest>=8.0", "pytest>=8.0",

View File

@@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""
Extract all Azure role names from rbacLookup.bicep and generate Python code
for _KNOWN_ENUMS in modules.py.
Usage:
python3 scripts/extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep
"""
import re
import sys
from pathlib import Path
def extract_role_names(bicep_file: Path) -> list[str]:
"""Extract all role names from the rbacLookup.bicep var roles = {...} block."""
content = bicep_file.read_text()
# Find the "var roles = {" block
roles_match = re.search(r'@export\(\)\s*var\s+roles\s*=\s*\{(.+?)\n\}', content, re.DOTALL)
if not roles_match:
raise ValueError("Could not find 'var roles = {' block in Bicep file")
roles_block = roles_match.group(1)
# Extract all role names (keys before the colon)
# Pattern: " ROLE_NAME: 'guid'"
role_names = re.findall(r'^\s+([A-Z_]+):', roles_block, re.MULTILINE)
return sorted(role_names)
def generate_python_code(role_names: list[str]) -> str:
"""Generate Python code for _KNOWN_ENUMS["roles"]."""
lines = ['_KNOWN_ENUMS = {']
lines.append(' "roles": [')
for role in role_names:
lines.append(f' "{role}",')
lines.append(' ],')
lines.append(' "roleDefinitionIds": [ # alias for roles')
for role in role_names:
lines.append(f' "{role}",')
lines.append(' ],')
lines.append('}')
return '\n'.join(lines)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: extract_roles_from_rbaclookup.py /path/to/rbacLookup.bicep", file=sys.stderr)
sys.exit(1)
bicep_path = Path(sys.argv[1])
if not bicep_path.exists():
print(f"Error: File not found: {bicep_path}", file=sys.stderr)
sys.exit(1)
try:
roles = extract_role_names(bicep_path)
print(f"# Extracted {len(roles)} roles from {bicep_path.name}")
print(f"# Generated: {Path(__file__).name}\n")
print(generate_python_code(roles))
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)

View File

@@ -17,7 +17,6 @@ frames straight to stdout.
import asyncio import asyncio
import ssl import ssl
import sys import sys
import traceback
import websockets import websockets
@@ -72,9 +71,8 @@ async def main(uri: str) -> None:
if msg is None: if msg is None:
break break
await ws.send(msg) await ws.send(msg)
except Exception as e: except Exception:
print(f"stdin_to_ws error: {e}", file=sys.stderr) pass
traceback.print_exc(file=sys.stderr)
async def ws_to_stdout() -> None: async def ws_to_stdout() -> None:
try: try:
@@ -82,9 +80,8 @@ async def main(uri: str) -> None:
data = frame if isinstance(frame, bytes) else frame.encode() data = frame if isinstance(frame, bytes) else frame.encode()
sys.stdout.buffer.write(data) sys.stdout.buffer.write(data)
sys.stdout.buffer.flush() sys.stdout.buffer.flush()
except Exception as e: except Exception:
print(f"ws_to_stdout error: {e}", file=sys.stderr) pass
traceback.print_exc(file=sys.stderr)
await asyncio.gather(stdin_to_ws(), ws_to_stdout()) await asyncio.gather(stdin_to_ws(), ws_to_stdout())
@@ -93,9 +90,4 @@ if __name__ == "__main__":
if len(sys.argv) < 2: if len(sys.argv) < 2:
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr) print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
sys.exit(1) sys.exit(1)
try:
asyncio.run(main(sys.argv[1])) asyncio.run(main(sys.argv[1]))
except Exception as e:
print(f"Fatal error: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
sys.exit(1)

View File

@@ -1,12 +0,0 @@
#!/bin/bash
# Wrapper for lsp_bridge.py that logs to a file for debugging IntelliJ issues
LOG_FILE="/tmp/lsp_bridge_debug.log"
echo "=== LSP Bridge started at $(date) ===" >> "$LOG_FILE"
echo "Args: $@" >> "$LOG_FILE"
echo "PWD: $(pwd)" >> "$LOG_FILE"
echo "Python: $(which python3)" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
exec /opt/homebrew/bin/python3 /Users/lrihni/Projects/iLSP/scripts/lsp_bridge.py "$@" 2>> "$LOG_FILE"

View File

@@ -31,6 +31,7 @@ BICEP_CATALOG="$DEVOPS_MCP_REPO/bicep_modules_catalog.json"
IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json" IAC_CATALOG="$DEVOPS_MCP_REPO/iac_source_catalog.json"
ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)" ILSP_REPO="$(cd "$(dirname "$0")/.." && pwd)"
TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json" TMPL_CATALOG="$ILSP_REPO/pipeline_templates_catalog.json"
PRINCIPALS_CATALOG="$ILSP_REPO/principals_catalog.json"
echo "── iLSP catalog push ──────────────────────────────" echo "── iLSP catalog push ──────────────────────────────"
@@ -50,6 +51,13 @@ if [[ ! -f "$TMPL_CATALOG" ]]; then
fi fi
echo "$(basename "$TMPL_CATALOG") ($(du -sh "$TMPL_CATALOG" | cut -f1))" echo "$(basename "$TMPL_CATALOG") ($(du -sh "$TMPL_CATALOG" | cut -f1))"
if [[ ! -f "$PRINCIPALS_CATALOG" ]]; then
echo " ✗ Not found: $PRINCIPALS_CATALOG"
echo " Run: python3 $ILSP_REPO/scripts/sync_principals_catalog.py"
exit 1
fi
echo "$(basename "$PRINCIPALS_CATALOG") ($(du -sh "$PRINCIPALS_CATALOG" | cut -f1))"
# Copy iac_source_catalog.json to iLSP repo root so it gets baked into the Docker image # Copy iac_source_catalog.json to iLSP repo root so it gets baked into the Docker image
echo "" echo ""
echo " → Copying iac_source_catalog.json to iLSP repo root (for Docker bake) …" echo " → Copying iac_source_catalog.json to iLSP repo root (for Docker bake) …"
@@ -59,7 +67,7 @@ echo " ✓ Copied to $ILSP_REPO/iac_source_catalog.json"
echo "" echo ""
echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …" echo " → Copying to $AUTOBOX:$REMOTE_DIR/ …"
ssh "$AUTOBOX" "mkdir -p $REMOTE_DIR" ssh "$AUTOBOX" "mkdir -p $REMOTE_DIR"
scp "$BICEP_CATALOG" "$IAC_CATALOG" "$TMPL_CATALOG" "$AUTOBOX:$REMOTE_DIR/" scp "$BICEP_CATALOG" "$IAC_CATALOG" "$TMPL_CATALOG" "$PRINCIPALS_CATALOG" "$AUTOBOX:$REMOTE_DIR/"
echo " ✓ Upload done" echo " ✓ Upload done"
if [[ "$NO_RELOAD" == "true" ]]; then if [[ "$NO_RELOAD" == "true" ]]; then
@@ -78,5 +86,5 @@ else
fi fi
echo "" echo ""
echo " Done. Bicep + YAML pipeline template completions updated." echo " Done. Bicep + YAML pipeline template + principals completions updated."
echo " Note: iac_source_catalog.json was copied to repo root — commit + push to bake into next Docker image." echo " Note: iac_source_catalog.json was copied to repo root — commit + push to bake into next Docker image."

View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
sync_principals_catalog.py — Build principals_catalog.json by scanning .bicep files
for array param values (GUIDs) and their inline comments.
Scans configured IaC repo directories for patterns like:
additionalAccess: ['c88bf29d-...'] // LRIADMPRO-IaC-Bicep
additionalAccess: [
'c88bf29d-...' // LRIADMPRO-IaC-Bicep
'another-guid' // Another-SP
]
Usage:
python3 scripts/sync_principals_catalog.py
python3 scripts/sync_principals_catalog.py --paths ~/IdeaProjects/Bitbucket/IaC
python3 scripts/sync_principals_catalog.py --dry-run
python3 scripts/sync_principals_catalog.py --output /path/to/principals_catalog.json
"""
import argparse
import json
import logging
import pathlib
import re
import sys
from datetime import datetime, timezone
from typing import Any
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger(__name__)
_REPO_ROOT = pathlib.Path(__file__).parent.parent
_DEFAULT_OUTPUT = _REPO_ROOT / "principals_catalog.json"
# Default paths to scan — adjust to match your IaC repo locations
_DEFAULT_SCAN_PATHS = [
"~/IdeaProjects/Bitbucket/IaC",
"~/IdeaProjects/Bitbucket/LRU",
]
# Matches a UUID/GUID
_GUID_RE = re.compile(
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
)
# Matches a single quoted GUID optionally followed by an inline comment:
# 'c88bf29d-...' // Some label text
# 'c88bf29d-...' // or with hash comments
_ITEM_RE = re.compile(
r"'(" + _GUID_RE.pattern + r")'\s*(?://+\s*(.+?)\s*)?$",
re.IGNORECASE,
)
# Matches the opening of an array param assignment:
# additionalAccess: [ or additionalAccess: ['guid'
_ARRAY_OPEN_RE = re.compile(r"^\s*(\w+)\s*:\s*\[")
def _extract_label(comment: str | None) -> str | None:
"""Clean up an inline comment to use as a display label."""
if not comment:
return None
# Strip trailing punctuation and whitespace
return comment.strip().rstrip(".,;")
def scan_file(path: pathlib.Path) -> dict[str, list[dict[str, Any]]]:
"""Scan a single .bicep file and return {param_name: [{id, label, source}]}."""
try:
text = path.read_text(encoding="utf-8")
except Exception as exc:
log.debug("Cannot read %s: %s", path, exc)
return {}
lines = text.splitlines()
results: dict[str, list[dict[str, Any]]] = {}
i = 0
while i < len(lines):
line = lines[i]
array_m = _ARRAY_OPEN_RE.match(line)
if not array_m:
i += 1
continue
param_name = array_m.group(1)
# Collect all characters on this and subsequent lines until array closes
collected = line[array_m.end() - 1:] # from '[' onwards
j = i + 1
# If the array doesn't close on the same line, keep accumulating
while collected.count("[") > collected.count("]") and j < len(lines):
collected += "\n" + lines[j]
j += 1
# Extract all GUID items from the collected block
for item_line in collected.splitlines():
m = _ITEM_RE.search(item_line)
if not m:
continue
guid = m.group(1).lower()
label = _extract_label(m.group(2))
entry: dict[str, Any] = {
"id": guid,
"label": label or guid,
"source": str(path),
}
if label:
entry["description"] = f"From {path.name}"
results.setdefault(param_name, [])
results[param_name].append(entry)
i = j
return results
def scan_paths(paths: list[pathlib.Path]) -> dict[str, list[dict[str, Any]]]:
"""Scan all .bicep files under the given paths, deduplicating GUIDs per param."""
# param_name → {guid → entry} (dict for dedup)
merged: dict[str, dict[str, dict[str, Any]]] = {}
files_scanned = 0
for base in paths:
if not base.exists():
log.warning("Path not found, skipping: %s", base)
continue
for bicep_file in sorted(base.rglob("*.bicep")):
file_results = scan_file(bicep_file)
files_scanned += 1
for param, entries in file_results.items():
bucket = merged.setdefault(param, {})
for entry in entries:
guid = entry["id"]
if guid not in bucket:
bucket[guid] = entry
else:
# Keep the entry with the most informative label
existing = bucket[guid]
if entry.get("label") and entry["label"] != guid:
if existing.get("label") == guid or not existing.get("label"):
bucket[guid] = entry
log.info("Scanned %d .bicep files across %d path(s)", files_scanned, len(paths))
# Flatten back to lists, sorted by label
return {
param: sorted(entries.values(), key=lambda e: e.get("label", e["id"]).lower())
for param, entries in sorted(merged.items())
}
def build_catalog(scan_paths_list: list[pathlib.Path]) -> dict[str, Any]:
params = scan_paths(scan_paths_list)
total = sum(len(v) for v in params.values())
log.info("Found %d unique entries across %d param(s)", total, len(params))
return {
"synced_at": datetime.now(timezone.utc).isoformat(),
"entry_count": total,
"params": params,
}
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--paths",
nargs="+",
default=_DEFAULT_SCAN_PATHS,
metavar="PATH",
help="Directories to scan for .bicep files (default: %(default)s)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print findings without writing the catalog",
)
parser.add_argument(
"--output",
default=str(_DEFAULT_OUTPUT),
help="Output JSON file (default: %(default)s)",
)
args = parser.parse_args()
resolved = [pathlib.Path(p).expanduser().resolve() for p in args.paths]
catalog = build_catalog(resolved)
if args.dry_run:
print(f"\n── Principals catalog (dry-run) ──────────────────────")
if not catalog["params"]:
print(" No GUID values found in .bicep files.")
for param, entries in catalog["params"].items():
print(f"\n param: {param} ({len(entries)} entries)")
for e in entries:
print(f" {e['label']:<40} {e['id']}")
print(f"\n Total: {catalog['entry_count']} entries")
return
out = pathlib.Path(args.output)
# Strip internal 'source' field — not needed at runtime
for entries in catalog["params"].values():
for e in entries:
e.pop("source", None)
out.write_text(json.dumps(catalog, indent=2, ensure_ascii=False), encoding="utf-8")
log.info("Written: %s (%d entries)", out, catalog["entry_count"])
if __name__ == "__main__":
main()

View File

@@ -1,132 +0,0 @@
import json
from ilsp.bicep_lsp.local_features import (
compute_folding_ranges,
compute_signature_help,
compute_workspace_symbols,
)
# ── signatureHelp ────────────────────────────────────────────────────────────
def test_signature_help_simple_call():
lines = ["var id = resourceId('Microsoft.Storage/storageAccounts', "]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is not None
sig = result["signatures"][0]
assert sig["label"].startswith("resourceId(")
assert result["activeParameter"] == 1
def test_signature_help_first_arg():
lines = ["var g = guid("]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is not None
assert result["activeParameter"] == 0
assert result["signatures"][0]["label"].startswith("guid(")
def test_signature_help_unknown_function_returns_none():
lines = ["var x = notARealFunction("]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is None
def test_signature_help_outside_call_returns_none():
lines = ["var x = 'hello world'"]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is None
def test_signature_help_nested_call_uses_innermost():
lines = ["var x = concat(toLower("]
result = compute_signature_help(lines, 0, len(lines[0]))
assert result is not None
assert result["signatures"][0]["label"].startswith("toLower(")
# ── foldingRange ─────────────────────────────────────────────────────────────
def test_folding_range_multiline_object():
lines = [
"resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {",
" name: 'demo'",
" sku: {",
" name: 'Standard_LRS'",
" }",
"}",
]
ranges = compute_folding_ranges(lines)
assert {"startLine": 0, "endLine": 5, "kind": "region"} in ranges
assert {"startLine": 2, "endLine": 4, "kind": "region"} in ranges
def test_folding_range_ignores_single_line_brackets():
lines = ["var x = { a: 1 }"]
ranges = compute_folding_ranges(lines)
assert ranges == []
def test_folding_range_comment_block():
lines = [
"// line one",
"// line two",
"// line three",
"var x = 1",
]
ranges = compute_folding_ranges(lines)
assert {"startLine": 0, "endLine": 2, "kind": "comment"} in ranges
def test_folding_range_short_comment_block_not_folded():
lines = ["// only one line", "var x = 1"]
ranges = compute_folding_ranges(lines)
assert not any(r["kind"] == "comment" for r in ranges)
# ── workspace/symbol ─────────────────────────────────────────────────────────
def test_workspace_symbol_finds_declarations():
docs = {
"file:///a.bicep": [
"param location string = 'westeurope'",
"var storageName = 'demo'",
"resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {",
" name: storageName",
"}",
"module roleAssignment 'br/modules:modules/roleassignments:2.0.x' = {",
" name: 'ra'",
"}",
],
}
results = compute_workspace_symbols(docs, "")
names = {r["name"] for r in results}
assert names == {"location", "storageName", "sa", "roleAssignment"}
def test_workspace_symbol_filters_by_query():
docs = {
"file:///a.bicep": [
"var storageName = 'demo'",
"var otherThing = 'x'",
],
}
results = compute_workspace_symbols(docs, "storage")
assert len(results) == 1
assert results[0]["name"] == "storageName"
def test_workspace_symbol_query_is_case_insensitive():
docs = {"file:///a.bicep": ["var StorageName = 'demo'"]}
results = compute_workspace_symbols(docs, "storagename")
assert len(results) == 1
def test_workspace_symbol_searches_across_multiple_documents():
docs = {
"file:///a.bicep": ["var fromA = 1"],
"file:///b.bicep": ["var fromB = 2"],
}
results = compute_workspace_symbols(docs, "")
names = {r["name"] for r in results}
assert names == {"fromA", "fromB"}

View File

@@ -18,6 +18,7 @@ def test_frame_produces_correct_header():
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def reset_modules(): def reset_modules():
BicepModuleCatalog._modules = [] BicepModuleCatalog._modules = []
BicepModuleCatalog._principals = {}
yield yield
@@ -173,12 +174,8 @@ def test_param_completions_injected_on_param_context():
assert "roleDefinitionIds" in labels assert "roleDefinitionIds" in labels
assert "principalId" in labels assert "principalId" in labels
assert "principalType" in labels assert "principalType" in labels
# First item is now the snippet completion assert items[0]["sortText"].startswith("0_lru_param_")
assert items[0]["label"] == "⚡ Fill params block" assert items[0]["kind"] == 5 # Field
assert items[0]["kind"] == 15 # Snippet
# Second item should be a regular param
assert items[1]["sortText"].startswith("0_lru_param_")
assert items[1]["kind"] == 5 # Field
def test_param_completion_items_have_insert_text(): def test_param_completion_items_have_insert_text():
@@ -186,12 +183,7 @@ def test_param_completion_items_have_insert_text():
"roleassignments", versions=["1.1.x"], schema=_ROLEASSIGNMENTS_SCHEMA "roleassignments", versions=["1.1.x"], schema=_ROLEASSIGNMENTS_SCHEMA
)] )]
items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x") items = BicepModuleCatalog.param_completion_items("roleassignments", "1.1.x")
# First item should be the snippet completion for item in items:
assert items[0]["label"] == "⚡ Fill params block"
assert items[0]["insertTextFormat"] == 2
assert "principalId" in items[0]["insertText"]
# Individual param items should have ": " suffix
for item in items[1:]:
assert item["insertText"].endswith(": ") assert item["insertText"].endswith(": ")
@@ -311,48 +303,6 @@ def test_detect_param_value_context_open_quote():
assert ctx["has_open_quote"] is True assert ctx["has_open_quote"] is True
def test_detect_param_value_context_in_array():
"""Cursor inside array value → param_value context."""
lines = [
"module myMod 'br/modules:roleassignments:1.1.x' = {",
" params: {",
" roles: ['KEY_VAULT_", # ← cursor inside array element
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character = len(" roles: ['KEY_VAULT_") = 23
ctx = session._detect_context(URI, {"line": 2, "character": 23})
assert ctx["type"] == "param_value"
assert ctx["module"] == "roleassignments"
assert ctx["param"] == "roles"
assert ctx["has_open_quote"] is True
def test_detect_param_value_context_in_multiline_array():
"""Cursor in multi-line array element → param_value context."""
lines = [
"module myMod 'br/modules:roleassignments:2.0.x' = {",
" params: {",
" assignments: [",
" {",
" roles: [",
" 'APP_CONFIGURATION_", # ← cursor on separate line
" ]",
" }",
" ]",
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character = len(" 'APP_CONFIGURATION_") = 27
ctx = session._detect_context(URI, {"line": 5, "character": 27})
assert ctx["type"] == "param_value"
assert ctx["module"] == "roleassignments"
assert ctx["param"] == "roles"
assert ctx["has_open_quote"] is True
def test_param_value_items_from_catalog_allowed(): def test_param_value_items_from_catalog_allowed():
"""environmentType completions come from catalog 'allowed' field.""" """environmentType completions come from catalog 'allowed' field."""
BicepModuleCatalog._modules = [_make_module( BicepModuleCatalog._modules = [_make_module(
@@ -404,24 +354,6 @@ def test_param_value_items_known_enum_fallback():
assert "User" in labels assert "User" in labels
def test_param_value_items_roles_enum():
"""roles parameter uses _KNOWN_ENUMS for Azure role completions."""
BicepModuleCatalog._modules = [_make_module(
"roleassignments",
versions=["1.1.x"],
schema={"1.1.x": {"parameters": {
"roles": {"type": "array"}, # no 'allowed' in catalog
}}},
)]
items = BicepModuleCatalog.param_value_completion_items(
"roleassignments", "1.1.x", "roles"
)
labels = [i["label"] for i in items]
assert "KEY_VAULT_SECRETS_USER" in labels
assert "STORAGE_BLOB_DATA_READER" in labels
assert "CONTRIBUTOR" in labels
def test_param_value_items_empty_for_free_string(): def test_param_value_items_empty_for_free_string():
"""A plain string param with no allowed values returns no completions.""" """A plain string param with no allowed values returns no completions."""
BicepModuleCatalog._modules = [_make_module( BicepModuleCatalog._modules = [_make_module(
@@ -455,6 +387,111 @@ def test_param_value_injected_in_completion_response():
assert labels == ["DEV", "TEST", "PROD"] assert labels == ["DEV", "TEST", "PROD"]
# ── Array item completion tests ────────────────────────────────────────────────
def test_detect_param_array_item_context():
"""Cursor inside [...] for an array param → param_array_item context."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: ['", # ← cursor after opening quote inside array
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
# character 24 = after the `'` (4 spaces + 16 "additionalAccess" + 2 ": " + 1 "[" + 1 "'" = 24)
ctx = session._detect_context(URI, {"line": 2, "character": 24})
assert ctx["type"] == "param_array_item"
assert ctx["module"] == "modules/keyvault"
assert ctx["param"] == "additionalAccess"
assert ctx["has_open_quote"] is True
def test_detect_param_array_item_context_no_quote():
"""Cursor at start of empty array → param_array_item, no open quote."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: [", # ← cursor after [
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
ctx = session._detect_context(URI, {"line": 2, "character": 23})
assert ctx["type"] == "param_array_item"
assert ctx["has_open_quote"] is False
def test_detect_param_array_item_context_after_existing_item():
"""Cursor after an existing item inside array → still param_array_item."""
lines = [
"module keyVault 'br/modules:modules/keyvault:2.1.x' = {",
" params: {",
" additionalAccess: ['c88bf29d-b13a-4153-9738-8995085a451e', '",
" }",
"}",
]
session = _make_session_with_doc(URI, lines)
char = len(" additionalAccess: ['c88bf29d-b13a-4153-9738-8995085a451e', '")
ctx = session._detect_context(URI, {"line": 2, "character": char})
assert ctx["type"] == "param_array_item"
assert ctx["has_open_quote"] is True
def test_param_array_item_completion_items():
"""param_array_item_completion_items returns entries from principals catalog."""
BicepModuleCatalog._principals = {
"additionalAccess": [
{"id": "aaaa-bbbb", "label": "My-SP", "description": "Test SP"},
]
}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess"
)
assert len(items) == 1
assert items[0]["label"] == "My-SP"
assert items[0]["detail"] == "aaaa-bbbb"
assert items[0]["insertText"] == "'aaaa-bbbb'"
def test_param_array_item_completion_items_open_quote():
BicepModuleCatalog._principals = {
"additionalAccess": [{"id": "aaaa-bbbb", "label": "My-SP"}]
}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess", has_open_quote=True
)
assert items[0]["insertText"] == "aaaa-bbbb'"
def test_param_array_item_empty_when_no_catalog_entry():
BicepModuleCatalog._principals = {}
items = BicepModuleCatalog.param_array_item_completion_items(
"modules/keyvault", "2.1.x", "additionalAccess"
)
assert items == []
def test_param_array_item_injected_in_completion_response():
"""Full pipeline: param_array_item context injects principal completions."""
BicepModuleCatalog._principals = {
"additionalAccess": [{"id": "aaaa-1111", "label": "IaC-SP", "description": "Pipeline SP"}]
}
msg = _completion_response([{"label": "noise", "sortText": "z"}])
ctx = {
"type": "param_array_item",
"module": "modules/keyvault",
"version": "2.1.x",
"param": "additionalAccess",
"has_open_quote": False,
}
out = json.loads(_inject_completions(msg, ctx))
items = out["result"]["items"]
assert len(items) == 1
assert items[0]["label"] == "IaC-SP"
assert items[0]["detail"] == "aaaa-1111"
def test_detect_unknown_context_outside_module(): def test_detect_unknown_context_outside_module():
lines = ["var x = 'hello'"] lines = ["var x = 'hello'"]
session = _make_session_with_doc(URI, lines) session = _make_session_with_doc(URI, lines)
@@ -502,7 +539,6 @@ def test_session_records_and_pops_context():
assert ctx["type"] == "param" assert ctx["type"] == "param"
assert ctx["module"] == "appservice" assert ctx["module"] == "appservice"
# Second pop (or any untracked id) returns None — the response must pass # Second pop returns unknown
# through untouched rather than being mistaken for a completion response. assert session.pop_context(42)["type"] == "unknown"
assert session.pop_context(42) is None