diff --git a/.claude/commands/commit.md b/.claude/commands/commit.md new file mode 100644 index 000000000..8c58345eb --- /dev/null +++ b/.claude/commands/commit.md @@ -0,0 +1,221 @@ +# Commit Workflow + +Prepare, verify, and commit code changes following project standards. + +## Workflow Overview + +``` +Verify → Fix Issues (TDD) → Pre-commit Checks → Commit → Tests → [Push] +``` + +## Quick Start Checklist + +Copy and track progress: + +``` +Commit Progress: +- [ ] Step 1: Run /verification +- [ ] Step 2: Fix issues using TDD +- [ ] Step 3: Run pre-commit checks +- [ ] Step 4: Create atomic commit +- [ ] Step 5: Run ALL test suites (3 REQUIRED): + - [ ] ./gradlew ktlintCheck spotlessApply + - [ ] ./gradlew koverXMLReport + - [ ] ./gradlew verifyPlugin +- [ ] Step 6: Push (optional, ask first) +``` + +**CRITICAL: Step 5 has THREE mandatory test suites. Skipping any is FORBIDDEN.** + +--- + +## Step 1: Run Verification + +Execute the `/verification` command to analyze code changes: + +``` +Verification Progress: +- [ ] Load project rules and standards +- [ ] Trace code paths for modified files +- [ ] Check for semantic changes +- [ ] Identify code smells +- [ ] Run security scans +- [ ] Review PR feedback (if PR exists) +``` + +**Output**: List of issues found, categorized by severity. + +--- + +## Step 2: Fix Issues Using TDD + +For each issue identified by verification: + +### TDD Gate (MANDATORY) + +Before ANY fix: + +- [ ] Write failing test first +- [ ] Confirm test fails without fix +- [ ] Implement minimal fix +- [ ] Confirm test passes + +**Never skip TDD. Use `/implementation` for complex fixes.** + +### Issue Priority + +1. **Security vulnerabilities** - Fix immediately (except test data) +2. **Breaking changes** - Confirm intentional, add tests +3. **Code smells** - Fix immediately +4. **PR feedback** - Address blockers first + +--- + +## Step 3: Pre-commit Checks + +Run: + +```bash +./gradlew ktlintCheck spotlessApply +./gradlew koverXMLReport +./gradlew verifyPlugin +``` + +### Security Scans + +Get absolute path first: + +```bash +pwd +``` + +Then run: + +1. `snyk_sca_scan` with absolute project path +2. `snyk_code_scan` with absolute project path + +**Fix any security issues** (skip test data false positives). + +--- + +## Step 4: Create Atomic Commit + +### Pre-commit Verification + +``` +- [ ] Linting clean (./gradlew ktlintCheck spotlessApply) +- [ ] Security scans clean +- [ ] No implementation plan files staged +- [ ] Documentation updated (if needed) +``` + +### Commit Format + +``` +type(scope): description [XXX-XXXX] + +Body explaining what and why. +``` + +**Types**: feat, fix, refactor, test, docs, chore, perf + +**Extract issue ID from branch:** + +```bash +git branch --show-current +``` + +### Staged Files Check + +```bash +git status +git diff --staged +``` + +**Never commit**: + +- Implementation plan files (`*_implementation_plan/`) +- Secrets or credentials +- Generated diagram source (commit PNGs only if needed) + +### Execute Commit + +```bash +git add +git commit -m "$(cat <<'EOF' +type(scope): description [XXX-XXXX] + +Body explaining what and why. +EOF +)" +``` + +**NEVER use --no-verify. NEVER amend commits.** + +--- + +## Step 5: Run All Test Suites (After Commit, Before Push) + +**CRITICAL: ALL three test suites MUST be executed after commit but before push. Skipping ANY is FORBIDDEN.** + +Run in order: + +```bash +./gradlew ktlintCheck spotlessApply +./gradlew koverXMLReport +./gradlew verifyPlugin +``` + +If ANY test suite fails: + +1. Do not proceed to push +2. Identify root cause +3. Apply TDD fix (test first, then implementation) +4. Create new commit with fix +5. Re-run ALL test suites + +--- + +## Step 6: Push (Optional) + +**Always ask before pushing.** + +If approved: + +```bash +git push --set-upstream origin $(git branch --show-current) +``` + +### After Push + +Offer to: + +1. Create draft PR (if none exists) +2. Update PR description (if PR exists) +3. Check snyk-pr-review-bot comments + +--- + +## Command Reference + +| Task | Command | +| ------------------- |----------------------------------------------------| +| Format & lint | `./gradlew ktlintCheck spotlessApply` | +| Unit tests | `./gradlew test` | +| Integration tests | `./gradlew verifyPlugin` | +| SCA scan | `snyk_sca_scan` with absolute path | +| Code scan | `snyk_code_scan` with absolute path | +| Current branch | `git branch --show-current` | +| Push | `git push --set-upstream origin $(git branch ...)` | + +--- + +## Red Flags (STOP) + +- [ ] Tests failing +- [ ] **Any test suite skipped** +- [ ] Security vulnerabilities unfixed +- [ ] Implementation plan files staged +- [ ] Unresolved PR blockers +- [ ] TDD not followed for fixes +- [ ] --no-verify being considered diff --git a/.claude/commands/create-implementation-plan.md b/.claude/commands/create-implementation-plan.md new file mode 100644 index 000000000..7fb8c6e2a --- /dev/null +++ b/.claude/commands/create-implementation-plan.md @@ -0,0 +1,110 @@ +# Create Implementation Plan + +Creates implementation plans using the official project template with session hand-off support, TDD workflow, and progress tracking. + +## Quick Start + +1. Extract issue ID from branch: `git branch --show-current` (format: `XXX-XXXX`) +2. Read Jira issue for context and acceptance criteria +3. Create plan file: `${issueID}_implementation_plan.md` (root directory) +4. Create `tests.json` for test scenario tracking +5. Create mermaid diagrams in `docs/diagrams/` +6. **STOP and wait for user confirmation** + +## Template Location + +Use template from: `.github/IMPLEMENTATION_PLAN_TEMPLATE.md` + +Replace all `{{TICKET_ID}}` and `{{TICKET_TITLE}}` placeholders with actual values. + +## Files to Create + +| File | Location | Purpose | +| ------------------- | ----------------------------------- | ------------------------------------ | +| Implementation plan | `${issueID}_implementation_plan.md` | Main plan document | +| Test tracking | `tests.json` | Track test scenarios across sessions | +| Flow diagrams | `docs/diagrams/${issueID}_*.mmd` | Mermaid source files | + +**All these files are gitignored - NEVER commit them.** + +## Key Sections to Complete + +### 1. SESSION RESUME (Critical for hand-off) + +- Update Quick Context with ticket info and branch +- Fill Current State table +- List Next Actions +- Update Current Working Files table + +### 2. Phase 1: Planning + +- **1.1 Requirements Analysis**: List changes, error handling, files to modify/create +- **1.2 Schema/Architecture Design**: Add schemas, data structures +- **1.3 Flow Diagrams**: Create mermaid files, generate PNGs + +### 3. Phase 2: Implementation (Outside-in TDD) + +- **CRITICAL: use outside-in TDD** +- Enforce strict test order: + 1. Smoke tests (E2E behavior) + 2. Integration tests (cross operating system behaviour, integrative behaviour) + 3. Unit tests +- Break into checkpoint steps (completable in one session) +- Each step: tasks, tests to write FIRST, commit message +- Reference test IDs from `tests.json` +- Add a plan self-check: integration tests must appear before unit tests + +### 4. Phase 3: Review + +- Code review prep checklist +- Documentation updates +- Pre-commit checks + +### 5. Progress Tracking + +- Update status table at end of each session +- Add entry to Session Log + +## tests.json Structure + +```json +{ + "ticket": "IDE-XXXX", + "description": "Ticket title", + "lastUpdated": "YYYY-MM-DD", + "lastSession": { + "date": "YYYY-MM-DD", + "sessionNumber": 1, + "completedSteps": [], + "currentStep": "1.1 Requirements Analysis", + "nextStep": "1.2 Schema Design" + }, + "testSuites": { + "unit": {}, + "integration": { "scenarios": [] }, + "regression": { "scenarios": [] } + } +} +``` + +## Diagram Creation + +1. Create: `docs/diagrams/${issueID}_description.mmd` +2. Reference PNG in plan: `![Name](docs/diagrams/${issueID}_description.png)` + +## Critical Rules + +- **NEVER commit** implementation plan, tests.json, or plan diagrams +- **WAIT for confirmation** after creating the plan before implementing +- **Use Outside-in TDD** - write tests FIRST +- **Update progress** at end of EVERY session (hand-off support) +- **Update Jira** with progress comments +- **Sync** plan changes to Jira ticket description and Confluence (if applicable) + +## Workflow Integration + +This command is called by `/implementation` when no plan exists. After creating the plan: + +1. Present plan summary to user +2. Wait for confirmation +3. `/implementation` continues with Phase 2 (Implementation) diff --git a/.claude/commands/implementation.md b/.claude/commands/implementation.md new file mode 100644 index 000000000..5752e4f79 --- /dev/null +++ b/.claude/commands/implementation.md @@ -0,0 +1,180 @@ +# Start Implementation Task + +## Workflow Overview + +``` +Check Plan → [Create if missing] → TEST FIRST → Implement → Test & Lint → Commit → Session Hand-off +``` + +**TDD is NON-NEGOTIABLE**: Every code change requires a failing test BEFORE implementation. + +## Phase 1: Initialize + +### 1.1 Get Issue Context + +```bash +git branch --show-current +``` + +The `issueID` follows format `XXX-XXXX` (e.g., `IDE-1718`). + +### 1.2 Check for Implementation Plan + +Look for: `${issueID}_implementation_plan/${issueID}_implementation_plan.md` + +**If plan exists:** Read it, note current progress, continue from last checkpoint. + +**If no plan:** Use `/create-implementation-plan` to create one. + +--- + +## Phase 2: Planning + +### Analysis + +- **Files to modify:** [list files] +- **Files to create:** [list files] +- **Packages affected:** [list packages] + +### Flow Diagrams + +See: `docs/diagrams/${issueID}_*.png` + +--- + +## Phase 3: Implementation (TDD) + +### CRITICAL: TDD is MANDATORY + +**NEVER write production code before writing a failing test.** + +This applies to: +- New features +- Bug fixes +- Security fixes +- Refactoring +- ANY code change + +### TDD Gate Check + +Before writing ANY production code, verify: + +- [ ] **Test exists?** Have I written a test for this change? +- [ ] **Test fails?** Does the test fail without my change? +- [ ] **Test is specific?** Does the test target the exact behavior I'm changing? + +**If ANY answer is NO → STOP and write the test first.** + +### TDD Cycle + +For each feature/change: + +1. **STOP** - Do not touch production code yet +2. **Write failing test first** (outside-in: start with integration/smoke tests, then unit tests) +3. **Run test** - confirm it fails for the right reason +4. **Write minimal code** to make `./gradlew test` pass +5. **Run test** - confirm it passes +6. **Refactor** if needed (tests must still pass) + +### Test Order (Outside-In) + +1. Smoke tests (E2E behavior) +2. Integration tests +3. Unit tests + +### Commands + +```bash +# Run unit tests +./gradlew test + +# Run smoke tests / integration tests +./gradlew verifyPlugin + +# Format and lint +./gradlew ktlintCheck spotlessApply +``` + +### Progress Updates + +Before each step: + +```markdown +| Step 1 | **in-progress** | Started [time] | +``` + +After each step: + +```markdown +| Step 1 | **completed** | Finished [time] | +``` + +--- + +## Phase 4: Finalize + +### 4.1 Run All Tests + +```bash +./gradlew test +./gradlew verifyPlugin +``` + +All tests must pass. Fix any failures before proceeding. + +### 4.2 Lint + +```bash +./gradlew ktlintCheck spotlessApply +``` + +Zero linting errors required. + +### 4.3 Security Scan + +Run before commit: + +- `snyk_code_scan` with absolute project path +- `snyk_sca_scan` with absolute project path + +Fix any security issues (except in test data). + +### 4.4 Generate & Update Docs + +Update documentation in `./docs` as needed. + +### 4.5 Commit + +Pre-commit checklist: + +- [ ] Tests pass (pre-existing issues MUST be fixed) +- [ ] Linting clean +- [ ] Security scans clean +- [ ] Docs updated + +Commit format: + +``` +type(scope): description [XXX-XXXX] + +Body explaining what and why. +``` + +**Never skip hooks. Never use --no-verify.** + +--- + +## Phase 5: Session Hand-off + +Update implementation plan with session summary according to the plan session handoff section. + +--- + +## Quick Reference + +| Action | Command | +| ----------------- | ------------------------------------- | +| Unit tests | `./gradlew test` | +| Smoke tests | `./gradlew verifyPlugin` | +| Format & lint | `./gradlew ktlintCheck spotlessApply` | +| Coverage | `./gradlew koverXmlReport` | diff --git a/.claude/commands/verification.md b/.claude/commands/verification.md new file mode 100644 index 000000000..7b6c8c0d6 --- /dev/null +++ b/.claude/commands/verification.md @@ -0,0 +1,239 @@ +# Code Verification + +Verify generated code in depth before committing. This command complements the pre-commit checklist by adding semantic analysis. + +## When to Use + +- Before committing implementation changes +- After completing implementation steps +- When PR review feedback needs to be addressed +- When explicitly asked to verify code +- When starting a new session of an implementation plan + +## Verification Workflow + +Copy this checklist and track progress: + +``` +Verification Progress: +- [ ] Step 1: Load project rules and standards +- [ ] Step 2: Trace code paths for modified files +- [ ] Step 3: Check for semantic changes +- [ ] Step 4: Identify code smells +- [ ] Step 5: Run security scans +- [ ] Step 6: Review PR feedback (if PR exists) +- [ ] Step 7: Get check results from github with gh cli +- [ ] Step 8: Update implementation plan with findings +- [ ] Step 9: Fix issues (TDD REQUIRED - test first, then fix) +- [ ] Step 10: Check coverage of changed files > 80% +- [ ] Step 11: Add tests if coverage not sufficient +- [ ] Step 12: Commit changes +``` + +--- + +## Step 1: Load Project Rules + +Read and apply these project standards: + +1. `CLAUDE.md` - critical rules and workflow +2. `.github/CONTRIBUTING.md` - coding standards + +Key rules to verify against: + +- Outside-in TDD followed +- Minimum necessary changes +- No workarounds or commented-out code +- mockk used for mocking (no custom mocks) + +--- + +## Step 2: Trace Code Paths + +For each modified file, trace the execution flow: + +1. **Identify entry points**: API handlers, public functions, exported methods +2. **Follow the call chain**: Map function calls through the codebase +3. **Verify dependencies**: Check that all called functions exist and have correct signatures +4. **Check return paths**: Ensure all code paths return appropriate values/errors + +### Verification Questions + +- Does the new code integrate correctly with existing callers? +- Are all error cases handled? +- Do interface implementations satisfy their contracts? +- Are there unreachable code paths? + +--- + +## Step 3: Check for Semantic Changes + +Detect unintended behavioral changes: + +### Breaking Changes + +- Modified function signatures +- Changed return types or error conditions +- Altered class/data class field types +- Modified interface definitions + +### Behavioral Changes + +- Different error messages (may break client parsing) +- Changed response structure +- Modified validation logic +- Altered default values + +### API Impact + +- API contract changes requiring versioning +- Behavior changes requiring documentation updates + +**Action**: Flag any semantic changes and ask if they are intentional. + +--- + +## Step 4: Identify Code Smells + +Check for these patterns: + +### Structural Smells + +- [ ] Functions longer than 50 lines +- [ ] Deeply nested conditionals (>3 levels) +- [ ] Duplicate code blocks +- [ ] God objects/functions doing too much +- [ ] Long parameter lists (>5 params) + +### Kotlin-Specific Smells + +- [ ] Ignored exceptions (empty catch blocks) +- [ ] Non-null assertions (`!!`) without justification +- [ ] Mutable state shared across threads without synchronization +- [ ] Memory leaks (listeners not unregistered, resources not closed) + +### Design Smells + +- [ ] Circular dependencies between packages +- [ ] Leaky abstractions (implementation details exposed) +- [ ] Inappropriate intimacy (packages knowing too much about each other) +- [ ] Feature envy (functions using other package's data excessively) + +### Copy-Paste Code (Refactoring Candidates) + +Identify duplicated code that should be extracted: + +- [ ] Similar code blocks across multiple files +- [ ] Repeated struct/data class transformations +- [ ] Duplicated validation logic +- [ ] Repeated error handling patterns +- [ ] Similar test setup code + +**Action**: For each smell found, propose a specific fix or flag for discussion. + +--- + +## Step 5: Run Security Scans + +Execute security checks using Snyk: + +```bash +pwd +``` + +Then run: + +1. `snyk_sca_scan` - dependency vulnerabilities +2. `snyk_code_scan` - code security issues + +### Manual Security Checklist + +- [ ] No hardcoded secrets, tokens, or credentials +- [ ] Input validation on all external data +- [ ] No path traversal vulnerabilities +- [ ] Proper authentication/authorization checks +- [ ] Sensitive data not logged +- [ ] HTTPS/TLS used for external calls + +**Action**: Fix security issues. If in test data, note but don't fix. + +--- + +## Step 6: Review PR Feedback + +If a GitHub PR exists for the current branch: + +```bash +gh pr view --json number,reviews,comments,url 2>/dev/null +``` + +For each review comment: + +1. **Categorize**: Bug | Enhancement | Style | Question | Blocker +2. **Assess**: Is this actionable? Does it require a decision? +3. **Prioritize**: Critical (must fix) | Should fix | Nice to have + +--- + +## Step 7: Update Implementation Plan + +Add verification findings to the implementation plan: + +```markdown +## Verification Results + +### Code Path Analysis + +- [List traced paths and any issues found] + +### Semantic Changes + +- [List any behavioral changes detected] + +### Code Smells + +- [List smells found with proposed fixes] + +### Security Findings + +- [List security issues and resolutions] + +### PR Feedback Items + +- [List items requiring decisions] +``` + +--- + +## Step 9: Fix Issues (TDD Required) + +**CRITICAL: ALL fixes MUST follow TDD. NEVER implement a fix without writing a failing test first.** + +When verification identifies issues to fix: + +1. Write a test that exposes the issue +2. Run the test - confirm it FAILS +3. Apply the minimum change to make the test pass +4. Run the test - confirm it PASSES +5. Run all test suites to verify no regressions + +--- + +## Quick Reference + +### Commands + +| Task | Command | +| --------- | ------------------------------------------- | +| Check PR | `gh pr view --json number,reviews,comments` | +| SCA scan | `snyk_sca_scan` with absolute path | +| Code scan | `snyk_code_scan` with absolute path | +| Run tests | `./gradlew test` | + +### Red Flags (Stop and Discuss) + +- Breaking API changes without versioning +- Security vulnerabilities in non-test code +- Significant behavioral changes +- Unresolved PR blockers +- Significant code duplication (>20 lines copied) diff --git a/.gitignore b/.gitignore index 63823cb45..722c9af99 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ Thumbs.db .cursor/rules/snyk_rules.mdc docs/performance-analysis.md IDE-* + +# Claude Code local settings (machine-specific, not shared) +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1346ff560 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# Project Rules + +## General + +- Always be concise, direct and don't try to appease me. +- Use .github/CONTRIBUTING.md and the links in there to find standards and contributing guidelines. +- DOUBLE CHECK THAT YOUR CHANGES ARE REALLY NEEDED. ALWAYS STICK TO THE GIVEN GOAL, NOT MORE. +- Don't optimize, don't refactor if not needed. +- Adhere to the rules, fix linting & test issues that are newly introduced. +- The `issueID` is usually specified in the current branch in the format `XXX-XXXX`. +- Read the issue description and acceptance criteria from jira (the manually given prompt takes precedence). + +## Process + +- Always create an implementation plan and save it to the directory under `${issueID}_implementation_plan` but never commit it. +- You will find a template for an implementation plan in `.github`. +- The implementation plan should have the phases: planning, implementation (including testing through TDD), review. +- Get confirmation that the implementation plan is ok. Wait until you get it. +- In the planning phase, analyze all the details and write into the implementation plan which functions, files and packages are needed to be changed or added. +- Be detailed: add steps to the phases and prepare a tracking section with checkboxes for progress tracking of each detailed step. +- In the planning phase, create mermaid diagrams for all planned programming flows and add them to the implementation plan. +- Use the same name for the diagrams as the implementation plan, but with the right extension (.mmd), so that they are ignored via .gitignore. +- Generate the implementation plan diagrams by putting the mermaid files into docs/diagrams and generate the pngs via mmdc with `-w 2048px` and add the flows to the implementation plan. +- Never commit the diagrams generated for the implementation plan. + +## Coding Guidelines + +- Follow the implementation plan step-by-step, phase-by-phase. Take it as a reference for each step and how to proceed. +- Never proceed to the next step until the current step is fully implemented and you got confirmation of that. +- Never jump a step. Always follow the plan. +- Use atomic commits. +- Update progress of the step before starting with a step and when ending. +- Update the jira ticket with the current status & progress (comment). +- USE TDD. +- I REPEAT: USE TDD. +- Always write and update test cases before writing the implementation (Test Driven Development). Iterate until they pass. +- After changing .kt or .java files, run `./gradlew spotlessCheck ktlintCheck` to check formatting and lint. Only continue once they pass. +- Always verify if fixes worked by running `./gradlew test`. +- Do atomic commits, see committing section for details. Ask before committing an atomic commit. +- Update current status in the implementation plan (in progress work, finished work, next steps). +- Maintain existing code patterns and conventions. +- Use mockk to mock. Writing your own mocks is forbidden if mockk can be used. +- Re-use mocks. +- Don't change code that does not need to be changed. Only do the minimum changes. +- Don't comment what is done, instead comment why something is done if the code is not clear. +- Use `./gradlew test` to run tests. +- Achieve 80% of test coverage. Use `./gradlew koverXmlReport`. +- If files are not used or needed anymore, delete them instead of deprecating them. +- Ask the human whether to maintain backwards compatibility or not. +- If a tool call fails, analyze why it failed and correct your approach. Don't prompt the user for help. +- If you don't know something, read the code instead of assuming it. +- Commenting out code to fix errors is NEVER a solution. Fix the error. +- Disabling or removing tests IS NOT ALLOWED. This can only be done manually by a human. +- Disabling linters is not allowed unless the human EXPLICITLY allows it for that single instance. +- Don't do workarounds. +- ALWAYS create production-ready code. We don't want examples, we want working, production-ready code. + +## Security + +- Determine the absolute path of the project directory (e.g., by executing `pwd`). +- Pass the absolute path of the project directory as a parameter to snyk_sca_scan and snyk_code_scan. +- Run snyk_sca_scan after updating gradle.build.kts. +- Run snyk_sca_scan and snyk_code_scan before committing. If not test data, fix issues before committing. +- Fix security issues if they are fixable. Take the snyk scan results and the test results as input. +- Don't fix test data. + +## Committing + +- NEVER commit implementation plan and implementation plan diagrams. +- NEVER amend commits, keep a history so we can revert atomic commits. +- NEVER NEVER NEVER skip the commit hooks. +- I REPEAT: NEVER USE --no-verify. DO NOT DO IT. NEVER. THIS IS CRITICAL, DO NOT DO IT. +- Run `./gradlew test` before committing and fix the issues. Don't run targeted tests, run the full suite (which may take >10min). +- Test failures prevent committing, regardless if caused by our changes. They MUST be fixed, even if they existed before. +- Deactivating tests is NEVER ALLOWED. +- Check with Kover (`./gradlew koverXmlReport`) that coverage of changed files is 80%+. +- Update the documentation before committing. +- When asked to commit, always use conventional commit messages (Conventional Commit Style: Subject + Body). Be descriptive in the body. If you find a JIRA issue (XXX-XXXX) in the branch name, use it as a postfix to the subject line in the format `[XXX-XXXX]`. +- Consider all commits in the current branch when committing, to have the context of the current changes. + +## Pushing + +- Before pushing, run `./gradlew verifyPlugin`. +- Never push without asking every single time. +- Never force push. +- When asked to push, always use `git push --set-upstream origin `. +- Regularly fetch main branch and offer to merge it into the current branch. +- After pushing offer to create a PR on github if no PR already exists. Analyze the changes by comparing the current branch with origin/main, and craft a PR description and title. +- Use the github template in `.github/PULL_REQUEST_TEMPLATE.md`. + +## PR Creation + +- Use `gh` command line util for PR creation. +- Use the template in `.github`. +- Always create draft PRs. +- Update the github PR description with the current status using `gh` command line util. +- Use the diff between the current branch and main to generate the description and title. +- Respect the PR template. +- Get the PR review comments, analyse them and propose fixes for them. Check before each commit. + +## Documenting + +- Always keep the documentation up-to-date in `./docs`. +- Don't create summary mds unless asked. +- Create mermaid syntax for all programming flows and add it to the documentation in `./docs`. +- Create png files from the mermaid diagrams using mmdc with `-w 2048px` for high resolution. +- Document the tested scenarios for all testing stages (unit, integration, e2e) in `./docs`. diff --git a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt index 69f223575..ae8771a18 100644 --- a/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt +++ b/src/main/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateService.kt @@ -25,7 +25,7 @@ class SnykApplicationSettingsStateService : // events var pluginInstalledSent: Boolean = false - val requiredLsProtocolVersion = 24 + val requiredLsProtocolVersion = 25 @Deprecated("left for old users migration only") var useTokenAuthentication = false var authenticationType = AuthenticationType.OAUTH2 @@ -40,6 +40,14 @@ class SnykApplicationSettingsStateService : // testing flag var fileListenerEnabled: Boolean = true + var explicitChanges: MutableSet = mutableSetOf() + + fun markExplicitlyChanged(settingKey: String) { + explicitChanges.add(settingKey) + } + + fun isExplicitlyChanged(settingKey: String): Boolean = explicitChanges.contains(settingKey) + // TODO migrate to // https://plugins.jetbrains.com/docs/intellij/persisting-sensitive-data.html?from=jetbrains.org var token: String? = null diff --git a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt index 16b1b086f..20a3bb28c 100644 --- a/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt +++ b/src/main/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurable.kt @@ -27,6 +27,8 @@ import io.snyk.plugin.ui.settings.HTMLSettingsPanel import javax.swing.JComponent import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.withSetting class SnykProjectSettingsConfigurable(val project: Project) : SearchableConfigurable { private val settingsStateService @@ -233,12 +235,18 @@ fun applyFolderConfigChanges( val existingConfig = fcs.getFolderConfig(folderPath) val updatedConfig = - existingConfig.copy( - additionalParameters = ParametersListUtil.parse(additionalParameters), - // Clear the preferredOrg field if the auto org selection is enabled. - preferredOrg = if (autoSelectOrgEnabled) "" else preferredOrgText.trim(), - orgSetByUser = !autoSelectOrgEnabled, - ) + existingConfig + .withSetting( + LsFolderSettingsKeys.ADDITIONAL_PARAMETERS, + ParametersListUtil.parse(additionalParameters), + changed = true, + ) + .withSetting( + LsFolderSettingsKeys.PREFERRED_ORG, + if (autoSelectOrgEnabled) "" else preferredOrgText.trim(), + changed = true, + ) + .withSetting(LsFolderSettingsKeys.ORG_SET_BY_USER, !autoSelectOrgEnabled, changed = true) fcs.addFolderConfig(updatedConfig) } diff --git a/src/main/kotlin/io/snyk/plugin/ui/ReferenceChooserDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/ReferenceChooserDialog.kt index 0d1697a6f..bc9283af8 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/ReferenceChooserDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/ReferenceChooserDialog.kt @@ -16,13 +16,15 @@ import java.awt.GridBagLayout import javax.swing.JComponent import javax.swing.JLabel import javax.swing.JPanel -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.LspFolderConfig +import snyk.common.lsp.settings.withSetting class ReferenceChooserDialog(val project: Project) : DialogWrapper(true) { - var baseBranches: MutableMap> = mutableMapOf() - internal var referenceFolders: MutableMap = + var baseBranches: MutableMap> = mutableMapOf() + internal var referenceFolders: MutableMap = mutableMapOf() internal var hasChanges = false @@ -46,11 +48,13 @@ class ReferenceChooserDialog(val project: Project) : DialogWrapper(true) { folderConfigs.forEach { folderConfig -> // Only create combo box if there are local branches - folderConfig.localBranches + (folderConfig.settings?.get(LsFolderSettingsKeys.LOCAL_BRANCHES)?.value as? List<*>) + ?.filterIsInstance() ?.takeIf { it.isNotEmpty() } ?.let { localBranches -> val comboBox = ComboBox(localBranches.sorted().toTypedArray()) - comboBox.selectedItem = folderConfig.baseBranch + comboBox.selectedItem = + folderConfig.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value as? String ?: "" comboBox.name = folderConfig.folderPath // Add change listener to track modifications @@ -81,9 +85,10 @@ class ReferenceChooserDialog(val project: Project) : DialogWrapper(true) { return scrollPane } - private fun configureReferenceFolder(folderConfig: FolderConfig): TextFieldWithBrowseButton { + private fun configureReferenceFolder(folderConfig: LspFolderConfig): TextFieldWithBrowseButton { val referenceFolder = TextFieldWithBrowseButton() - referenceFolder.text = folderConfig.referenceFolderPath ?: "" + referenceFolder.text = + folderConfig.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value as? String ?: "" referenceFolder.name = folderConfig.folderPath referenceFolder.toolTipText = @@ -141,10 +146,13 @@ class ReferenceChooserDialog(val project: Project) : DialogWrapper(true) { // Update if either base branch or reference folder is provided if (baseBranch.isNotBlank() || referenceFolderControl?.text?.isNotBlank() == true) { folderConfigSettings.addFolderConfig( - folderConfig.copy( - baseBranch = baseBranch, - referenceFolderPath = referenceFolderControl?.text ?: "", - ) + folderConfig + .withSetting(LsFolderSettingsKeys.BASE_BRANCH, baseBranch, changed = true) + .withSetting( + LsFolderSettingsKeys.REFERENCE_FOLDER, + referenceFolderControl?.text ?: "", + changed = true, + ) ) } } @@ -156,7 +164,9 @@ class ReferenceChooserDialog(val project: Project) : DialogWrapper(true) { referenceFolders[folderConfig]?.text?.let { referenceFolder -> if (referenceFolder.isNotBlank()) { folderConfigSettings.addFolderConfig( - folderConfig.copy(baseBranch = "", referenceFolderPath = referenceFolder) + folderConfig + .withSetting(LsFolderSettingsKeys.BASE_BRANCH, "", changed = true) + .withSetting(LsFolderSettingsKeys.REFERENCE_FOLDER, referenceFolder, changed = true) ) } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt index 732586a16..5ea86cb90 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/SnykSettingsDialog.kt @@ -71,9 +71,10 @@ import javax.swing.event.DocumentEvent import javax.swing.text.BadLocationException import org.jetbrains.concurrency.runAsync import snyk.SnykBundle -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.LspFolderConfig class SnykSettingsDialog( private val project: Project, @@ -211,7 +212,10 @@ class SnykSettingsDialog( cliReleaseChannelDropDown.selectedItem = applicationSettings.cliReleaseChannel baseBranchInfoLabel.text = service().getAll().values.joinToString("\n") { - "${it.folderPath}: Reference branch: ${it.baseBranch}, Reference directory: ${it.referenceFolderPath}" + val branch = it.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value as? String ?: "" + val refDir = + it.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value as? String ?: "" + "${it.folderPath}: Reference branch: $branch, Reference directory: $refDir" } netNewIssuesDropDown.selectedItem = applicationSettings.issuesToDisplay } @@ -258,7 +262,9 @@ class SnykSettingsDialog( cliReleaseChannelDropDown.selectedItem = settings.cliReleaseChannel baseBranchInfoLabel.text = service().getAll().values.joinToString("\n") { - "${it.folderPath}: Reference branch: ${it.baseBranch}, Reference directory: ${it.referenceFolderPath}" + val branch = it.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value as? String ?: "" + val refDir = it.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value as? String ?: "" + "${it.folderPath}: Reference branch: $branch, Reference directory: $refDir" } netNewIssuesDropDown.selectedItem = settings.issuesToDisplay @@ -955,7 +961,7 @@ class SnykSettingsDialog( fun isAutoSelectOrgEnabled(): Boolean = autoDetectOrgCheckbox.isSelected - private fun getFolderConfig(): FolderConfig? { + private fun getFolderConfig(): LspFolderConfig? { val folderConfigSettings = service() val languageServerWrapper = LanguageServerWrapper.getInstance(project) return languageServerWrapper @@ -973,10 +979,11 @@ class SnykSettingsDialog( val organization = if (autoDetectOrgSelected) { // Checkbox checked = auto-detect enabled = use autoDeterminedOrg only - folderConfig?.autoDeterminedOrg ?: "" + folderConfig?.settings?.get(LsFolderSettingsKeys.AUTO_DETERMINED_ORG)?.value as? String + ?: "" } else { // Checkbox unchecked = manual selection = clear textbox for user input - folderConfig?.preferredOrg ?: "" + folderConfig?.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value as? String ?: "" } preferredOrgTextField.text = organization diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/ConfigurationDataClasses.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/ConfigurationDataClasses.kt index 20e0bc8a2..8265eb6c5 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/ConfigurationDataClasses.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/ConfigurationDataClasses.kt @@ -113,6 +113,16 @@ data class FolderConfigData( @SerializedName("orgSetByUser") val orgSetByUser: Boolean? = null, @SerializedName("scanCommandConfig") val scanCommandConfig: Map? = null, + // Org-scope override fields + @SerializedName("scanAutomatic") val scanAutomatic: Boolean? = null, + @SerializedName("scanNetNew") val scanNetNew: Boolean? = null, + @SerializedName("enabledSeverities") val enabledSeverities: SeverityFilterConfig? = null, + @SerializedName("snykOssEnabled") val snykOssEnabled: Boolean? = null, + @SerializedName("snykCodeEnabled") val snykCodeEnabled: Boolean? = null, + @SerializedName("snykIacEnabled") val snykIacEnabled: Boolean? = null, + @SerializedName("issueViewOpenIssues") val issueViewOpenIssues: Boolean? = null, + @SerializedName("issueViewIgnoredIssues") val issueViewIgnoredIssues: Boolean? = null, + @SerializedName("riskScoreThreshold") val riskScoreThreshold: Int? = null, ) data class ScanCommandConfigData( diff --git a/src/main/kotlin/io/snyk/plugin/ui/jcef/SaveConfigHandler.kt b/src/main/kotlin/io/snyk/plugin/ui/jcef/SaveConfigHandler.kt index 8314330f2..17c20474f 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/jcef/SaveConfigHandler.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/jcef/SaveConfigHandler.kt @@ -21,6 +21,9 @@ import org.cef.browser.CefFrame import org.cef.handler.CefLoadHandlerAdapter import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.LsSettingsKeys +import snyk.common.lsp.settings.withSetting import snyk.trust.WorkspaceTrustService class SaveConfigHandler( @@ -199,28 +202,56 @@ class SaveConfigHandler( ) { val isFallback = config.isFallbackForm == true - config.manageBinariesAutomatically?.let { settings.manageBinariesAutomatically = it } + config.manageBinariesAutomatically?.let { + settings.manageBinariesAutomatically = it + settings.markExplicitlyChanged(LsSettingsKeys.AUTOMATIC_DOWNLOAD) + } // Use the provided cliPath from the config if present, or the default CLI path if not. - config.cliPath?.let { path -> settings.cliPath = path.ifEmpty { getDefaultCliPath() } } + config.cliPath?.let { path -> + settings.cliPath = path.ifEmpty { getDefaultCliPath() } + settings.markExplicitlyChanged(LsSettingsKeys.CLI_PATH) + } - config.cliBaseDownloadURL?.let { settings.cliBaseDownloadURL = it } + config.cliBaseDownloadURL?.let { + settings.cliBaseDownloadURL = it + settings.markExplicitlyChanged(LsSettingsKeys.BINARY_BASE_URL) + } config.cliReleaseChannel?.let { settings.cliReleaseChannel = it } - config.insecure?.let { settings.ignoreUnknownCA = it } + config.insecure?.let { + settings.ignoreUnknownCA = it + settings.markExplicitlyChanged(LsSettingsKeys.PROXY_INSECURE) + } if (!isFallback) { settings.ossScanEnable = config.activateSnykOpenSource ?: false + settings.markExplicitlyChanged(LsSettingsKeys.SNYK_OSS_ENABLED) settings.snykCodeSecurityIssuesScanEnable = config.activateSnykCode ?: false + settings.markExplicitlyChanged(LsSettingsKeys.SNYK_CODE_ENABLED) settings.iacScanEnabled = config.activateSnykIac ?: false + settings.markExplicitlyChanged(LsSettingsKeys.SNYK_IAC_ENABLED) settings.secretsEnabled = config.activateSnykSecrets ?: false + settings.markExplicitlyChanged(LsSettingsKeys.SNYK_SECRETS_ENABLED) // Scanning mode - config.scanningMode?.let { settings.scanOnSave = (it == "auto") } + config.scanningMode?.let { + settings.scanOnSave = (it == "auto") + settings.markExplicitlyChanged(LsSettingsKeys.SCAN_AUTOMATIC) + } // Connection settings - config.organization?.let { settings.organization = it } - config.endpoint?.let { settings.customEndpointUrl = it } - config.token?.let { settings.token = it } + config.organization?.let { + settings.organization = it + settings.markExplicitlyChanged(LsSettingsKeys.ORGANIZATION) + } + config.endpoint?.let { + settings.customEndpointUrl = it + settings.markExplicitlyChanged(LsSettingsKeys.API_ENDPOINT) + } + config.token?.let { + settings.token = it + settings.markExplicitlyChanged(LsSettingsKeys.TOKEN) + } // Authentication method config.authenticationMethod?.let { method -> @@ -231,27 +262,52 @@ class SaveConfigHandler( "pat" -> AuthenticationType.PAT else -> AuthenticationType.OAUTH2 } + settings.markExplicitlyChanged(LsSettingsKeys.AUTHENTICATION_METHOD) } // Severity filters config.filterSeverity?.let { severity -> - severity.critical?.let { settings.criticalSeverityEnabled = it } - severity.high?.let { settings.highSeverityEnabled = it } - severity.medium?.let { settings.mediumSeverityEnabled = it } - severity.low?.let { settings.lowSeverityEnabled = it } + severity.critical?.let { + settings.criticalSeverityEnabled = it + settings.markExplicitlyChanged(LsSettingsKeys.ENABLED_SEVERITIES) + } + severity.high?.let { + settings.highSeverityEnabled = it + settings.markExplicitlyChanged(LsSettingsKeys.ENABLED_SEVERITIES) + } + severity.medium?.let { + settings.mediumSeverityEnabled = it + settings.markExplicitlyChanged(LsSettingsKeys.ENABLED_SEVERITIES) + } + severity.low?.let { + settings.lowSeverityEnabled = it + settings.markExplicitlyChanged(LsSettingsKeys.ENABLED_SEVERITIES) + } } // Issue view options config.issueViewOptions?.let { options -> - options.openIssues?.let { settings.openIssuesEnabled = it } - options.ignoredIssues?.let { settings.ignoredIssuesEnabled = it } + options.openIssues?.let { + settings.openIssuesEnabled = it + settings.markExplicitlyChanged(LsSettingsKeys.ISSUE_VIEW_OPEN_ISSUES) + } + options.ignoredIssues?.let { + settings.ignoredIssuesEnabled = it + settings.markExplicitlyChanged(LsSettingsKeys.ISSUE_VIEW_IGNORED_ISSUES) + } } // Delta findings - config.enableDeltaFindings?.let { settings.setDeltaEnabled(it) } + config.enableDeltaFindings?.let { + settings.setDeltaEnabled(it) + settings.markExplicitlyChanged(LsSettingsKeys.SCAN_NET_NEW) + } // Risk score threshold - config.riskScoreThreshold?.let { settings.riskScoreThreshold = it } + config.riskScoreThreshold?.let { + settings.riskScoreThreshold = it + settings.markExplicitlyChanged(LsSettingsKeys.RISK_SCORE_THRESHOLD) + } // Trusted folders - sync the list (add new, remove missing) config.trustedFolders?.let { folders -> @@ -308,21 +364,72 @@ class SaveConfigHandler( val fcs = service() for (folderConfig in folderConfigs) { - val existingConfig = fcs.getFolderConfig(folderConfig.folderPath) - - // Build updated config, writing values directly from config (use defaults if null) - val updatedConfig = - existingConfig.copy( - additionalParameters = folderConfig.additionalParameters, - additionalEnv = folderConfig.additionalEnv, - preferredOrg = folderConfig.preferredOrg ?: "", - autoDeterminedOrg = folderConfig.autoDeterminedOrg ?: "", - orgSetByUser = folderConfig.orgSetByUser ?: false, - scanCommandConfig = - folderConfig.scanCommandConfig?.let { parseScanCommandConfig(it) } - ?: existingConfig.scanCommandConfig, - ) - fcs.addFolderConfig(updatedConfig) + var updated = fcs.getFolderConfig(folderConfig.folderPath) + + // Apply each field from the UI form, marking as changed + folderConfig.additionalParameters?.let { + updated = + updated.withSetting(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS, it, changed = true) + } + folderConfig.additionalEnv?.let { + updated = + updated.withSetting(LsFolderSettingsKeys.ADDITIONAL_ENVIRONMENT, it, changed = true) + } + folderConfig.preferredOrg?.let { + updated = updated.withSetting(LsFolderSettingsKeys.PREFERRED_ORG, it, changed = true) + } + folderConfig.autoDeterminedOrg?.let { + updated = updated.withSetting(LsFolderSettingsKeys.AUTO_DETERMINED_ORG, it) + } + folderConfig.orgSetByUser?.let { + updated = updated.withSetting(LsFolderSettingsKeys.ORG_SET_BY_USER, it, changed = true) + } + folderConfig.scanCommandConfig?.let { + updated = + updated.withSetting( + LsFolderSettingsKeys.SCAN_COMMAND_CONFIG, + parseScanCommandConfig(it), + changed = true, + ) + } + + // Org-scope overrides from processFolderOverrides() in JS + folderConfig.scanAutomatic?.let { + updated = updated.withSetting(LsSettingsKeys.SCAN_AUTOMATIC, it, changed = true) + } + folderConfig.scanNetNew?.let { + updated = updated.withSetting(LsSettingsKeys.SCAN_NET_NEW, it, changed = true) + } + folderConfig.enabledSeverities?.let { + val sev = + snyk.common.lsp.settings.SeverityFilter( + critical = it.critical, + high = it.high, + medium = it.medium, + low = it.low, + ) + updated = updated.withSetting(LsSettingsKeys.ENABLED_SEVERITIES, sev, changed = true) + } + folderConfig.snykOssEnabled?.let { + updated = updated.withSetting(LsSettingsKeys.SNYK_OSS_ENABLED, it, changed = true) + } + folderConfig.snykCodeEnabled?.let { + updated = updated.withSetting(LsSettingsKeys.SNYK_CODE_ENABLED, it, changed = true) + } + folderConfig.snykIacEnabled?.let { + updated = updated.withSetting(LsSettingsKeys.SNYK_IAC_ENABLED, it, changed = true) + } + folderConfig.issueViewOpenIssues?.let { + updated = updated.withSetting(LsSettingsKeys.ISSUE_VIEW_OPEN_ISSUES, it, changed = true) + } + folderConfig.issueViewIgnoredIssues?.let { + updated = updated.withSetting(LsSettingsKeys.ISSUE_VIEW_IGNORED_ISSUES, it, changed = true) + } + folderConfig.riskScoreThreshold?.let { + updated = updated.withSetting(LsSettingsKeys.RISK_SCORE_THRESHOLD, it, changed = true) + } + + fcs.addFolderConfig(updated) } } diff --git a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt index cb0954d4e..3debfe2d1 100644 --- a/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt +++ b/src/main/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowPanel.kt @@ -72,12 +72,13 @@ import org.jetbrains.concurrency.runAsync import snyk.common.ProductType import snyk.common.SnykError import snyk.common.lsp.AiFixParams -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.LsProduct import snyk.common.lsp.ScanIssue import snyk.common.lsp.SnykScanParams import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.LspFolderConfig /** Main panel for Snyk tool window. */ @Service(Service.Level.PROJECT) @@ -100,12 +101,16 @@ class SnykToolWindowPanel(val project: Project) : JPanel(), Disposable { Tree(rootTreeNode).apply { this.isRootVisible = pluginSettings().isDeltaFindingsEnabled() } } - private fun getRootNodeText(folderConfig: FolderConfig): String { + private fun getRootNodeText(folderConfig: LspFolderConfig): String { + val refPath = + folderConfig.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value as? String ?: "" + val branch = + folderConfig.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value as? String ?: "" val detail = - if (folderConfig.referenceFolderPath.isNullOrBlank()) { - folderConfig.baseBranch + if (refPath.isBlank()) { + branch } else { - folderConfig.referenceFolderPath + refPath } val path = folderConfig.folderPath.toNioPathOrNull() return "Click to choose base branch or reference folder for ${path?.fileName ?: path.toString()}: [ current: $detail ]" diff --git a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt index e33586b15..957e451b2 100644 --- a/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt +++ b/src/main/kotlin/snyk/common/lsp/LanguageServerWrapper.kt @@ -33,6 +33,7 @@ import java.util.concurrent.TimeoutException import java.util.concurrent.locks.ReentrantLock import java.util.logging.Level import java.util.logging.Logger.getLogger +import org.apache.commons.lang3.SystemUtils import org.eclipse.lsp4j.ClientCapabilities import org.eclipse.lsp4j.ClientInfo import org.eclipse.lsp4j.CodeActionCapabilities @@ -76,10 +77,11 @@ import snyk.common.lsp.commands.COMMAND_WORKSPACE_CONFIGURATION import snyk.common.lsp.commands.COMMAND_WORKSPACE_FOLDER_SCAN import snyk.common.lsp.commands.SNYK_GENERATE_ISSUE_DESCRIPTION import snyk.common.lsp.progress.ProgressManager +import snyk.common.lsp.settings.ConfigSetting import snyk.common.lsp.settings.FolderConfigSettings -import snyk.common.lsp.settings.IssueViewOptions -import snyk.common.lsp.settings.LanguageServerSettings -import snyk.common.lsp.settings.SeverityFilter +import snyk.common.lsp.settings.InitializationOptions +import snyk.common.lsp.settings.LsSettingsKeys +import snyk.common.lsp.settings.LspConfigurationParam import snyk.common.removeSuffix import snyk.pluginInfo import snyk.trust.WorkspaceTrustService @@ -325,7 +327,7 @@ class LanguageServerWrapper(private val project: Project) : Disposable { params.processId = ProcessHandle.current().pid().toInt() params.clientInfo = ClientInfo(pluginInfo.integrationEnvironment, pluginInfo.integrationEnvironmentVersion) - params.initializationOptions = getSettings() + params.initializationOptions = getInitializationOptions() params.capabilities = getCapabilities() initializeResult = @@ -532,13 +534,139 @@ class LanguageServerWrapper(private val project: Project) : Disposable { } } - fun getSettings(): LanguageServerSettings { + fun getSettings(): LspConfigurationParam { val ps = pluginSettings() - // only send folderConfig after having received the folderConfigs from LS - // IntelliJ only has in-memory storage, so that storage should not overwrite - // the folderConfigs in language server - val folderConfigs = + val settingsMap = mutableMapOf() + + // Global settings mapped to canonical pflag names + settingsMap[LsSettingsKeys.SNYK_CODE_ENABLED] = + ConfigSetting( + value = ps.snykCodeSecurityIssuesScanEnable, + changed = ps.isExplicitlyChanged(LsSettingsKeys.SNYK_CODE_ENABLED), + ) + settingsMap[LsSettingsKeys.SNYK_OSS_ENABLED] = + ConfigSetting( + value = ps.ossScanEnable, + changed = ps.isExplicitlyChanged(LsSettingsKeys.SNYK_OSS_ENABLED), + ) + settingsMap[LsSettingsKeys.SNYK_IAC_ENABLED] = + ConfigSetting( + value = ps.iacScanEnabled, + changed = ps.isExplicitlyChanged(LsSettingsKeys.SNYK_IAC_ENABLED), + ) + settingsMap[LsSettingsKeys.SNYK_SECRETS_ENABLED] = + ConfigSetting( + value = ps.secretsEnabled, + changed = ps.isExplicitlyChanged(LsSettingsKeys.SNYK_SECRETS_ENABLED), + ) + settingsMap[LsSettingsKeys.PROXY_INSECURE] = + ConfigSetting( + value = ps.ignoreUnknownCA, + changed = ps.isExplicitlyChanged(LsSettingsKeys.PROXY_INSECURE), + ) + + val endpoint = getEndpointUrl() + if (!endpoint.isNullOrBlank()) { + settingsMap[LsSettingsKeys.API_ENDPOINT] = + ConfigSetting( + value = endpoint, + changed = ps.isExplicitlyChanged(LsSettingsKeys.API_ENDPOINT), + ) + } + + if (ps.organization != null) { + settingsMap[LsSettingsKeys.ORGANIZATION] = + ConfigSetting( + value = ps.organization!!, + changed = ps.isExplicitlyChanged(LsSettingsKeys.ORGANIZATION), + ) + } + + settingsMap[LsSettingsKeys.SEND_ERROR_REPORTS] = ConfigSetting(value = true, changed = false) + settingsMap[LsSettingsKeys.AUTOMATIC_DOWNLOAD] = + ConfigSetting( + value = ps.manageBinariesAutomatically, + changed = ps.isExplicitlyChanged(LsSettingsKeys.AUTOMATIC_DOWNLOAD), + ) + + val currentCliPath = getCliFile().absolutePath + if (currentCliPath.isNotBlank()) { + settingsMap[LsSettingsKeys.CLI_PATH] = + ConfigSetting( + value = currentCliPath, + changed = ps.isExplicitlyChanged(LsSettingsKeys.CLI_PATH), + ) + } + + if (!ps.cliBaseDownloadURL.isNullOrBlank()) { + settingsMap[LsSettingsKeys.BINARY_BASE_URL] = + ConfigSetting( + value = ps.cliBaseDownloadURL, + changed = ps.isExplicitlyChanged(LsSettingsKeys.BINARY_BASE_URL), + ) + } + + if (!ps.token.isNullOrBlank()) { + settingsMap[LsSettingsKeys.TOKEN] = ConfigSetting(value = ps.token!!, changed = true) + } + + settingsMap[LsSettingsKeys.AUTOMATIC_AUTHENTICATION] = + ConfigSetting(value = false, changed = true) + + // filters + val severityFilter = + mapOf( + "critical" to ps.criticalSeverityEnabled, + "high" to ps.highSeverityEnabled, + "medium" to ps.mediumSeverityEnabled, + "low" to ps.lowSeverityEnabled, + ) + settingsMap[LsSettingsKeys.ENABLED_SEVERITIES] = + ConfigSetting( + value = severityFilter, + changed = ps.isExplicitlyChanged(LsSettingsKeys.ENABLED_SEVERITIES), + ) + + if (ps.riskScoreThreshold != null) { + settingsMap[LsSettingsKeys.RISK_SCORE_THRESHOLD] = + ConfigSetting( + value = ps.riskScoreThreshold!!, + changed = ps.isExplicitlyChanged(LsSettingsKeys.RISK_SCORE_THRESHOLD), + ) + } + + settingsMap[LsSettingsKeys.ISSUE_VIEW_OPEN_ISSUES] = + ConfigSetting( + value = ps.openIssuesEnabled, + changed = ps.isExplicitlyChanged(LsSettingsKeys.ISSUE_VIEW_OPEN_ISSUES), + ) + settingsMap[LsSettingsKeys.ISSUE_VIEW_IGNORED_ISSUES] = + ConfigSetting( + value = ps.ignoredIssuesEnabled, + changed = ps.isExplicitlyChanged(LsSettingsKeys.ISSUE_VIEW_IGNORED_ISSUES), + ) + + settingsMap[LsSettingsKeys.TRUST_ENABLED] = ConfigSetting(value = false, changed = true) + settingsMap[LsSettingsKeys.SCAN_AUTOMATIC] = + ConfigSetting( + value = ps.scanOnSave, + changed = ps.isExplicitlyChanged(LsSettingsKeys.SCAN_AUTOMATIC), + ) + settingsMap[LsSettingsKeys.AUTHENTICATION_METHOD] = + ConfigSetting( + value = ps.authenticationType.languageServerSettingsName, + changed = ps.isExplicitlyChanged(LsSettingsKeys.AUTHENTICATION_METHOD), + ) + settingsMap[LsSettingsKeys.ENABLE_SNYK_OSS_QUICK_FIX_CODE_ACTIONS] = + ConfigSetting(value = true, changed = false) + settingsMap[LsSettingsKeys.SCAN_NET_NEW] = + ConfigSetting( + value = ps.isDeltaFindingsEnabled(), + changed = ps.isExplicitlyChanged(LsSettingsKeys.SCAN_NET_NEW), + ) + + val folderConfigsList = configuredWorkspaceFolders .filter { val folderPath = it.uri.fromUriToPath().toString() @@ -550,42 +678,31 @@ class LanguageServerWrapper(private val project: Project) : Disposable { } .toList() + return LspConfigurationParam(settings = settingsMap, folderConfigs = folderConfigsList) + } + + fun getInitializationOptions(): InitializationOptions { + val ps = pluginSettings() val trustService = service() val trustedFolders = trustService.settings.getTrustedPaths() - return LanguageServerSettings( - activateSnykOpenSource = ps.ossScanEnable.toString(), - activateSnykCodeSecurity = ps.snykCodeSecurityIssuesScanEnable.toString(), - activateSnykIac = ps.iacScanEnabled.toString(), - activateSnykSecrets = ps.secretsEnabled.toString(), - organization = ps.organization ?: "", - insecure = ps.ignoreUnknownCA.toString(), - endpoint = getEndpointUrl(), - cliPath = getCliFile().absolutePath, - cliBaseDownloadURL = ps.cliBaseDownloadURL, - manageBinariesAutomatically = ps.manageBinariesAutomatically.toString(), - token = ps.token, - filterSeverity = - SeverityFilter( - critical = ps.criticalSeverityEnabled, - high = ps.highSeverityEnabled, - medium = ps.mediumSeverityEnabled, - low = ps.lowSeverityEnabled, - ), - issueViewOptions = - IssueViewOptions( - openIssues = ps.openIssuesEnabled, - ignoredIssues = ps.ignoredIssuesEnabled, - ), - enableTrustedFoldersFeature = "false", - scanningMode = if (!ps.scanOnSave) "manual" else "auto", + val param = getSettings() + + return InitializationOptions( + settings = param.settings, + folderConfigs = param.folderConfigs, + requiredProtocolVersion = ps.requiredLsProtocolVersion.toString(), + deviceId = ps.userAnonymousId, integrationName = pluginInfo.integrationName, integrationVersion = pluginInfo.integrationVersion, - authenticationMethod = ps.authenticationType.languageServerSettingsName, - enableSnykOSSQuickFixCodeActions = "true", - folderConfigs = folderConfigs, + osPlatform = SystemUtils.OS_NAME, + osArch = SystemUtils.OS_ARCH, + runtimeVersion = SystemUtils.JAVA_VERSION, + runtimeName = SystemUtils.JAVA_RUNTIME_NAME, + hoverVerbosity = 0, + outputFormat = "html", + path = null, trustedFolders = trustedFolders, - riskScoreThreshold = ps.riskScoreThreshold, ) } diff --git a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt index 8de0a4338..ff801e7eb 100644 --- a/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt +++ b/src/main/kotlin/snyk/common/lsp/SnykLanguageClient.kt @@ -63,6 +63,8 @@ import snyk.common.ProductType import snyk.common.editor.DocumentChanger import snyk.common.lsp.progress.ProgressManager import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsSettingsKeys +import snyk.common.lsp.settings.LspConfigurationParam import snyk.sdk.SdkHelper import snyk.trust.WorkspaceTrustService @@ -213,27 +215,201 @@ class SnykLanguageClient(private val project: Project, val progressManager: Prog return completedFuture } - @JsonNotification(value = "$/snyk.folderConfigs") - fun folderConfig(folderConfigParam: FolderConfigsParam?) { - if (disposed) return - val folderConfigs = folderConfigParam?.folderConfigs ?: emptyList() + @JsonNotification(value = "$/snyk.configuration") + fun snykConfiguration(configurationParam: LspConfigurationParam?) { + if (disposed || configurationParam == null) return runAsync { - val service = service() - val languageServerWrapper = LanguageServerWrapper.getInstance(project) + try { + val ps = pluginSettings() + var settingsChanged = false + + configurationParam.settings?.let { settings -> + settings[LsSettingsKeys.SNYK_CODE_ENABLED]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.snykCodeSecurityIssuesScanEnable != boolVal) { + ps.snykCodeSecurityIssuesScanEnable = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.SNYK_OSS_ENABLED]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.ossScanEnable != boolVal) { + ps.ossScanEnable = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.SNYK_IAC_ENABLED]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.iacScanEnabled != boolVal) { + ps.iacScanEnabled = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.SNYK_SECRETS_ENABLED]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.secretsEnabled != boolVal) { + ps.secretsEnabled = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.PROXY_INSECURE]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.ignoreUnknownCA != boolVal) { + ps.ignoreUnknownCA = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.API_ENDPOINT]?.value?.let { + (it as? String)?.let { strVal -> + if (ps.customEndpointUrl != strVal) { + ps.customEndpointUrl = strVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.ORGANIZATION]?.value?.let { + (it as? String)?.let { strVal -> + if (ps.organization != strVal) { + ps.organization = strVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.AUTOMATIC_DOWNLOAD]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.manageBinariesAutomatically != boolVal) { + ps.manageBinariesAutomatically = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.CLI_PATH]?.value?.let { + (it as? String)?.let { strVal -> + if (ps.cliPath != strVal) { + ps.cliPath = strVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.BINARY_BASE_URL]?.value?.let { + (it as? String)?.let { strVal -> + if (ps.cliBaseDownloadURL != strVal) { + ps.cliBaseDownloadURL = strVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.TOKEN]?.value?.let { + (it as? String)?.let { strVal -> + if (ps.token != strVal) { + ps.token = strVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.ENABLED_SEVERITIES]?.value?.let { + if (it is Map<*, *>) { + (it["critical"] as? Boolean)?.let { critical -> + if (ps.criticalSeverityEnabled != critical) { + ps.criticalSeverityEnabled = critical + settingsChanged = true + } + } + (it["high"] as? Boolean)?.let { high -> + if (ps.highSeverityEnabled != high) { + ps.highSeverityEnabled = high + settingsChanged = true + } + } + (it["medium"] as? Boolean)?.let { medium -> + if (ps.mediumSeverityEnabled != medium) { + ps.mediumSeverityEnabled = medium + settingsChanged = true + } + } + (it["low"] as? Boolean)?.let { low -> + if (ps.lowSeverityEnabled != low) { + ps.lowSeverityEnabled = low + settingsChanged = true + } + } + } + } + settings[LsSettingsKeys.RISK_SCORE_THRESHOLD]?.value?.let { + if (it is Number && ps.riskScoreThreshold != it.toInt()) { + ps.riskScoreThreshold = it.toInt() + settingsChanged = true + } + } + settings[LsSettingsKeys.ISSUE_VIEW_OPEN_ISSUES]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.openIssuesEnabled != boolVal) { + ps.openIssuesEnabled = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.ISSUE_VIEW_IGNORED_ISSUES]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.ignoredIssuesEnabled != boolVal) { + ps.ignoredIssuesEnabled = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.SCAN_AUTOMATIC]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.scanOnSave != boolVal) { + ps.scanOnSave = boolVal + settingsChanged = true + } + } + } + settings[LsSettingsKeys.SCAN_NET_NEW]?.value?.let { + (it as? Boolean)?.let { boolVal -> + if (ps.isDeltaFindingsEnabled() != boolVal) { + ps.setDeltaEnabled(boolVal) + settingsChanged = true + } + } + } + } - service.addAll(folderConfigs) - folderConfigs.forEach { languageServerWrapper.updateFolderConfigRefresh(it.folderPath, true) } + if (settingsChanged) { + StoreUtil.saveSettings(ApplicationManager.getApplication(), true) + logger.debug("force-saved settings from Language Server configuration") - // Migrate any nested folder configs that may have been created by earlier plugin versions - // Only workspace folder paths (non-nested) should have folder configs - service.migrateNestedFolderConfigs(project) + publishAsync(project, SnykSettingsListener.SNYK_SETTINGS_TOPIC) { settingsChanged() } + } - try { - // Already in runAsync, so just use sync publisher here - getSyncPublisher(project, SnykFolderConfigListener.SNYK_FOLDER_CONFIG_TOPIC) - ?.folderConfigsChanged(folderConfigs.isNotEmpty()) + configurationParam.folderConfigs?.let { folderConfigs -> + val service = service() + val languageServerWrapper = LanguageServerWrapper.getInstance(project) + + service.addAll(folderConfigs) + folderConfigs.forEach { + languageServerWrapper.updateFolderConfigRefresh(it.folderPath, true) + } + + // Migrate any nested folder configs that may have been created by earlier plugin versions + // Only workspace folder paths (non-nested) should have folder configs + service.migrateNestedFolderConfigs(project) + + try { + // Already in runAsync, so just use sync publisher here + getSyncPublisher(project, SnykFolderConfigListener.SNYK_FOLDER_CONFIG_TOPIC) + ?.folderConfigsChanged(folderConfigs.isNotEmpty()) + } catch (e: Exception) { + logger.error("Error processing snyk folder configs", e) + } + } } catch (e: Exception) { - logger.error("Error processing snyk folder configs", e) + logger.error("Error processing snyk configuration", e) } } } diff --git a/src/main/kotlin/snyk/common/lsp/Types.kt b/src/main/kotlin/snyk/common/lsp/Types.kt index 108e55c73..6acb51aa2 100644 --- a/src/main/kotlin/snyk/common/lsp/Types.kt +++ b/src/main/kotlin/snyk/common/lsp/Types.kt @@ -514,38 +514,6 @@ data class OssIdentifiers( } } -data class FolderConfigsParam( - @SerializedName("folderConfigs") val folderConfigs: List? -) - -/** - * FolderConfig stores the configuration for a workspace folder - * - * @param folderPath the path of the folder - * @param baseBranch the base branch to compare against (if git repository) - * @param localBranches the local branches in the git repository - * @param additionalParameters additional parameters to pass to the scan command - * @param additionalEnv additional environment variables to set for the scan command - * @param referenceFolderPath the reference folder to scan, if not a git repository - * @param scanCommandConfig the scan command configuration to specify a command to be executed - * before and/or after the scan - */ -data class FolderConfig( - @SerializedName("folderPath") val folderPath: String, - @SerializedName("preferredOrg") val preferredOrg: String = "", - @SerializedName("autoDeterminedOrg") val autoDeterminedOrg: String = "", - @SerializedName("baseBranch") val baseBranch: String, - @SerializedName("localBranches") val localBranches: List? = emptyList(), - @SerializedName("additionalParameters") val additionalParameters: List? = emptyList(), - @SerializedName("additionalEnv") val additionalEnv: String? = "", - @SerializedName("referenceFolderPath") val referenceFolderPath: String? = "", - @SerializedName("scanCommandConfig") - val scanCommandConfig: Map? = emptyMap(), - @SerializedName("orgSetByUser") val orgSetByUser: Boolean = false, -) : Comparable { - override fun compareTo(other: FolderConfig): Int = this.folderPath.compareTo(other.folderPath) -} - data class ScanCommandConfig( val preScanCommand: String = "", val preScanOnlyReferenceFolder: Boolean = true, diff --git a/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt b/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt index f3d959ce7..c02400fda 100644 --- a/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/settings/FolderConfigSettings.kt @@ -8,17 +8,16 @@ import com.intellij.openapi.ui.Messages import io.snyk.plugin.fromUriToPath import java.nio.file.Paths import java.util.concurrent.ConcurrentHashMap -import java.util.stream.Collectors import org.jetbrains.annotations.NotNull import snyk.SnykBundle -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper @Suppress("UselessCallOnCollection") @Service class FolderConfigSettings { private val logger = Logger.getInstance(FolderConfigSettings::class.java) - private val configs: MutableMap = ConcurrentHashMap() + private val configs: MutableMap = + ConcurrentHashMap() @Suppress( "UselessCallOnNotNull", @@ -26,22 +25,10 @@ class FolderConfigSettings { "UNNECESSARY_SAFE_CALL", "RedundantSuppression", ) - fun addFolderConfig(@NotNull folderConfig: FolderConfig) { + fun addFolderConfig(@NotNull folderConfig: LspFolderConfig) { if (folderConfig.folderPath.isNullOrBlank()) return val normalizedAbsolutePath = normalizePath(folderConfig.folderPath) - - // Handle null values from Language Server by providing defaults - val configToStore = - folderConfig.copy( - folderPath = normalizedAbsolutePath, - preferredOrg = folderConfig.preferredOrg ?: "", - autoDeterminedOrg = folderConfig.autoDeterminedOrg ?: "", - baseBranch = folderConfig.baseBranch ?: "", - referenceFolderPath = folderConfig.referenceFolderPath ?: "", - additionalParameters = folderConfig.additionalParameters ?: emptyList(), - additionalEnv = folderConfig.additionalEnv ?: "", - ) - configs[normalizedAbsolutePath] = configToStore + configs[normalizedAbsolutePath] = folderConfig.copy(folderPath = normalizedAbsolutePath) } private fun normalizePath(folderPath: String): String { @@ -49,25 +36,41 @@ class FolderConfigSettings { return normalizedAbsolutePath } - internal fun getFolderConfig(folderPath: String): FolderConfig { + internal fun getFolderConfig(folderPath: String): LspFolderConfig { val normalizedPath = normalizePath(folderPath) val folderConfig = configs[normalizedPath] ?: createEmpty(normalizedPath) return folderConfig } - private fun createEmpty(normalizedAbsolutePath: String): FolderConfig { - val newConfig = FolderConfig(folderPath = normalizedAbsolutePath, baseBranch = "main") - // Directly add to map, as addFolderConfig would re-normalize and copy, which is redundant here - // since normalizedAbsolutePath is already what we want for the key and the object's path. + private fun createEmpty(normalizedAbsolutePath: String): LspFolderConfig { + val newConfig = + LspFolderConfig( + folderPath = normalizedAbsolutePath, + settings = + mapOf( + LsFolderSettingsKeys.BASE_BRANCH to ConfigSetting(value = "main"), + LsFolderSettingsKeys.LOCAL_BRANCHES to ConfigSetting(value = emptyList()), + LsFolderSettingsKeys.ADDITIONAL_PARAMETERS to + ConfigSetting(value = emptyList()), + LsFolderSettingsKeys.ADDITIONAL_ENVIRONMENT to ConfigSetting(value = ""), + LsFolderSettingsKeys.REFERENCE_FOLDER to ConfigSetting(value = ""), + LsFolderSettingsKeys.PREFERRED_ORG to ConfigSetting(value = ""), + LsFolderSettingsKeys.AUTO_DETERMINED_ORG to ConfigSetting(value = ""), + LsFolderSettingsKeys.ORG_SET_BY_USER to ConfigSetting(value = false), + LsFolderSettingsKeys.SCAN_COMMAND_CONFIG to + ConfigSetting(value = emptyMap()), + ), + ) configs[normalizedAbsolutePath] = newConfig return newConfig } - fun getAll(): Map = HashMap(configs) + fun getAll(): Map = HashMap(configs) fun clear() = configs.clear() - fun addAll(folderConfigs: List) = folderConfigs.mapNotNull { addFolderConfig(it) } + fun addAll(folderConfigs: List) = + folderConfigs.mapNotNull { addFolderConfig(it) } /** * Gets all folder configs for a project. This method delegates to getFolderConfigs() to ensure @@ -76,8 +79,8 @@ class FolderConfigSettings { * @param project the project to get the folder configs for * @return the folder configs for workspace folders only (no nested paths) */ - fun getAllForProject(project: Project): List = - getFolderConfigs(project).stream().sorted().collect(Collectors.toList()).toList() + fun getAllForProject(project: Project): List = + getFolderConfigs(project).sortedBy { it.folderPath }.toList() /** * Gets the additional parameters for the given project by aggregating the folder configs with @@ -90,9 +93,12 @@ class FolderConfigSettings { // only use folder config with workspace folder path val additionalParameters = getFolderConfigs(project) - .filter { it.additionalParameters?.isNotEmpty() ?: false } - .mapNotNull { it.additionalParameters?.joinToString(" ") } - .joinToString(" ") + .map { config -> + (config.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value as? List<*>) + ?.filterIsInstance() ?: emptyList() + } + .filter { it.isNotEmpty() } + .joinToString(" ") { it.joinToString(" ") } return additionalParameters } @@ -103,7 +109,7 @@ class FolderConfigSettings { * @param project the project to get the folder configs for * @return the folder configs for the project */ - fun getFolderConfigs(project: Project): List { + fun getFolderConfigs(project: Project): List { val languageServerWrapper = LanguageServerWrapper.getInstance(project) return languageServerWrapper .getWorkspaceFoldersFromRoots(project, promptForTrust = false) @@ -123,7 +129,11 @@ class FolderConfigSettings { fun getPreferredOrg(project: Project): String { // Note - this will not work for projects with extra content roots outside of the the main // workspace folder. - return getFolderConfigs(project).map { it.preferredOrg }.firstOrNull() ?: "" + return getFolderConfigs(project) + .firstOrNull() + ?.settings + ?.get(LsFolderSettingsKeys.PREFERRED_ORG) + ?.value as? String ?: "" } /** @@ -134,7 +144,9 @@ class FolderConfigSettings { * @return true if auto-organization is enabled */ fun isAutoOrganizationEnabled(project: Project): Boolean = - getFolderConfigs(project).firstOrNull()?.orgSetByUser != true + getFolderConfigs(project).firstOrNull()?.let { + !(it.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value as? Boolean ?: false) + } ?: true /** * Sets the auto-organization setting for the given project. @@ -144,7 +156,12 @@ class FolderConfigSettings { */ fun setAutoOrganization(project: Project, autoOrganization: Boolean) { getFolderConfigs(project).forEach { folderConfig -> - val updatedConfig = folderConfig.copy(orgSetByUser = !autoOrganization) + val updatedConfig = + folderConfig.withSetting( + LsFolderSettingsKeys.ORG_SET_BY_USER, + !autoOrganization, + changed = true, + ) addFolderConfig(updatedConfig) } } @@ -157,7 +174,12 @@ class FolderConfigSettings { */ fun setOrganization(project: Project, organization: String?) { getFolderConfigs(project).forEach { folderConfig -> - val updatedConfig = folderConfig.copy(preferredOrg = organization ?: "") + val updatedConfig = + folderConfig.withSetting( + LsFolderSettingsKeys.PREFERRED_ORG, + organization ?: "", + changed = true, + ) addFolderConfig(updatedConfig) } } @@ -251,9 +273,9 @@ class FolderConfigSettings { private fun handleSingleCustomSubConfig( project: Project, parentPath: String, - parentConfig: FolderConfig, + parentConfig: LspFolderConfig, subPath: String, - subConfig: FolderConfig, + subConfig: LspFolderConfig, ): Int { val choice = promptForSingleSubConfigMigration(project, parentPath, subPath) @@ -285,7 +307,7 @@ class FolderConfigSettings { private fun handleMultipleConflictingConfigs( project: Project, parentPath: String, - customNestedConfigs: Map, + customNestedConfigs: Map, ): Int { val choice = promptForMultipleConflictingMigration(project, parentPath, customNestedConfigs.keys.toList()) @@ -394,28 +416,23 @@ class FolderConfigSettings { * user-configurable fields: baseBranch, referenceFolderPath, additionalParameters, additionalEnv, * preferredOrg, orgSetByUser, scanCommandConfig. */ - internal fun hasNonDefaultValues(config: FolderConfig, parentConfig: FolderConfig): Boolean = - config.baseBranch != parentConfig.baseBranch || - config.referenceFolderPath != parentConfig.referenceFolderPath || - config.additionalParameters != parentConfig.additionalParameters || - config.additionalEnv != parentConfig.additionalEnv || - config.preferredOrg != parentConfig.preferredOrg || - config.orgSetByUser != parentConfig.orgSetByUser || - config.scanCommandConfig != parentConfig.scanCommandConfig + internal fun hasNonDefaultValues( + config: LspFolderConfig, + parentConfig: LspFolderConfig, + ): Boolean = + COMPARABLE_SETTING_KEYS.any { key -> + config.settings?.get(key)?.value != parentConfig.settings?.get(key)?.value + } /** Checks if multiple configs have conflicting values between each other. */ - internal fun hasConflictingConfigs(configs: List): Boolean { + internal fun hasConflictingConfigs(configs: List): Boolean { if (configs.size < 2) return false val first = configs.first() return configs.drop(1).any { config -> - config.baseBranch != first.baseBranch || - config.referenceFolderPath != first.referenceFolderPath || - config.additionalParameters != first.additionalParameters || - config.additionalEnv != first.additionalEnv || - config.preferredOrg != first.preferredOrg || - config.orgSetByUser != first.orgSetByUser || - config.scanCommandConfig != first.scanCommandConfig + COMPARABLE_SETTING_KEYS.any { key -> + config.settings?.get(key)?.value != first.settings?.get(key)?.value + } } } @@ -423,16 +440,15 @@ class FolderConfigSettings { * Merges sub-config values into parent config. Sub-config values take precedence over parent * values. */ - internal fun mergeConfigs(parentConfig: FolderConfig, subConfig: FolderConfig): FolderConfig = - parentConfig.copy( - baseBranch = subConfig.baseBranch, - referenceFolderPath = subConfig.referenceFolderPath, - additionalParameters = subConfig.additionalParameters, - additionalEnv = subConfig.additionalEnv, - preferredOrg = subConfig.preferredOrg, - orgSetByUser = subConfig.orgSetByUser, - scanCommandConfig = subConfig.scanCommandConfig, - ) + internal fun mergeConfigs( + parentConfig: LspFolderConfig, + subConfig: LspFolderConfig, + ): LspFolderConfig { + // Merge sub-config settings into parent, sub-config values take precedence + val mergedSettings = (parentConfig.settings ?: emptyMap()).toMutableMap() + subConfig.settings?.forEach { (key, value) -> mergedSettings[key] = value } + return parentConfig.copy(settings = mergedSettings) + } /** * Checks if a path is nested under another path. @@ -457,4 +473,17 @@ class FolderConfigSettings { REMOVE_PARENT, KEEP_ALL } + + companion object { + private val COMPARABLE_SETTING_KEYS = + listOf( + LsFolderSettingsKeys.BASE_BRANCH, + LsFolderSettingsKeys.REFERENCE_FOLDER, + LsFolderSettingsKeys.ADDITIONAL_PARAMETERS, + LsFolderSettingsKeys.ADDITIONAL_ENVIRONMENT, + LsFolderSettingsKeys.PREFERRED_ORG, + LsFolderSettingsKeys.ORG_SET_BY_USER, + LsFolderSettingsKeys.SCAN_COMMAND_CONFIG, + ) + } } diff --git a/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt index ce8363e3e..ec43e9922 100644 --- a/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt +++ b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettings.kt @@ -1,61 +1,6 @@ -@file:Suppress("unused") - package snyk.common.lsp.settings import com.google.gson.annotations.SerializedName -import io.snyk.plugin.pluginSettings -import io.snyk.plugin.services.AuthenticationType -import org.apache.commons.lang3.SystemUtils -import snyk.common.lsp.FolderConfig -import snyk.pluginInfo - -data class LanguageServerSettings( - @SerializedName("activateSnykOpenSource") val activateSnykOpenSource: String? = "false", - @SerializedName("activateSnykCode") val activateSnykCode: String? = "false", - @SerializedName("activateSnykIac") val activateSnykIac: String? = "false", - @SerializedName("activateSnykSecrets") val activateSnykSecrets: String? = "false", - @SerializedName("insecure") val insecure: String?, - @SerializedName("endpoint") val endpoint: String?, - @SerializedName("additionalParams") val additionalParams: String? = null, - @SerializedName("additionalEnv") val additionalEnv: String? = null, - @SerializedName("path") val path: String? = null, - @SerializedName("sendErrorReports") val sendErrorReports: String? = "true", - @SerializedName("organization") val organization: String? = null, - @SerializedName("enableTelemetry") val enableTelemetry: String? = "false", - @SerializedName("manageBinariesAutomatically") val manageBinariesAutomatically: String? = "false", - @SerializedName("cliPath") val cliPath: String?, - @SerializedName("cliBaseDownloadURL") val cliBaseDownloadURL: String? = null, - @SerializedName("token") val token: String?, - @SerializedName("integrationName") val integrationName: String? = pluginInfo.integrationName, - @SerializedName("integrationVersion") - val integrationVersion: String? = pluginInfo.integrationVersion, - @SerializedName("automaticAuthentication") val automaticAuthentication: String? = "false", - @SerializedName("deviceId") val deviceId: String? = pluginSettings().userAnonymousId, - @SerializedName("filterSeverity") val filterSeverity: SeverityFilter? = null, - @SerializedName("issueViewOptions") val issueViewOptions: IssueViewOptions? = null, - @SerializedName("enableTrustedFoldersFeature") val enableTrustedFoldersFeature: String? = "false", - @SerializedName("trustedFolders") val trustedFolders: List? = emptyList(), - @SerializedName("activateSnykCodeSecurity") val activateSnykCodeSecurity: String? = "false", - @SerializedName("osPlatform") val osPlatform: String? = SystemUtils.OS_NAME, - @SerializedName("osArch") val osArch: String? = SystemUtils.OS_ARCH, - @SerializedName("runtimeVersion") val runtimeVersion: String? = SystemUtils.JAVA_VERSION, - @SerializedName("runtimeName") val runtimeName: String? = SystemUtils.JAVA_RUNTIME_NAME, - @SerializedName("scanningMode") val scanningMode: String? = null, - @SerializedName("authenticationMethod") - val authenticationMethod: String = AuthenticationType.OAUTH2.languageServerSettingsName, - @SerializedName("snykCodeApi") val snykCodeApi: String? = null, - @SerializedName("enableSnykLearnCodeActions") val enableSnykLearnCodeActions: String? = null, - @SerializedName("enableSnykOSSQuickFixCodeActions") - val enableSnykOSSQuickFixCodeActions: String? = null, - @SerializedName("requiredProtocolVersion") - val requiredProtocolVersion: String = pluginSettings().requiredLsProtocolVersion.toString(), - @SerializedName("hoverVerbosity") val hoverVerbosity: Int = 0, - @SerializedName("outputFormat") val outputFormat: String = "html", - @SerializedName("enableDeltaFindings") - val enableDeltaFindings: String = pluginSettings().isDeltaFindingsEnabled().toString(), - @SerializedName("folderConfigs") val folderConfigs: List = emptyList(), - @SerializedName("riskScoreThreshold") val riskScoreThreshold: Int? = null, -) data class SeverityFilter( @SerializedName("critical") val critical: Boolean?, @@ -68,3 +13,56 @@ data class IssueViewOptions( @SerializedName("openIssues") val openIssues: Boolean?, @SerializedName("ignoredIssues") val ignoredIssues: Boolean?, ) + +data class ConfigSetting( + @SerializedName("value") val value: Any, + @SerializedName("changed") val changed: Boolean? = null, + @SerializedName("source") val source: String? = null, + @SerializedName("originScope") val originScope: String? = null, + @SerializedName("isLocked") val isLocked: Boolean? = null, +) + +data class LspFolderConfig( + @SerializedName("folderPath") val folderPath: String, + @SerializedName("settings") val settings: Map? = null, +) + +fun LspFolderConfig.withSetting( + key: String, + value: Any, + changed: Boolean? = null, +): LspFolderConfig { + val newSettings = (settings ?: emptyMap()).toMutableMap() + val existing = newSettings[key] + newSettings[key] = + ConfigSetting( + value = value, + changed = changed ?: existing?.changed, + source = existing?.source, + originScope = existing?.originScope, + isLocked = existing?.isLocked, + ) + return copy(settings = newSettings) +} + +data class LspConfigurationParam( + @SerializedName("settings") val settings: Map? = null, + @SerializedName("folderConfigs") val folderConfigs: List? = null, +) + +data class InitializationOptions( + @SerializedName("settings") val settings: Map? = null, + @SerializedName("folderConfigs") val folderConfigs: List? = null, + @SerializedName("requiredProtocolVersion") val requiredProtocolVersion: String? = null, + @SerializedName("deviceId") val deviceId: String? = null, + @SerializedName("integrationName") val integrationName: String? = null, + @SerializedName("integrationVersion") val integrationVersion: String? = null, + @SerializedName("osPlatform") val osPlatform: String? = null, + @SerializedName("osArch") val osArch: String? = null, + @SerializedName("runtimeVersion") val runtimeVersion: String? = null, + @SerializedName("runtimeName") val runtimeName: String? = null, + @SerializedName("hoverVerbosity") val hoverVerbosity: Int? = null, + @SerializedName("outputFormat") val outputFormat: String? = null, + @SerializedName("path") val path: String? = null, + @SerializedName("trustedFolders") val trustedFolders: List? = emptyList(), +) diff --git a/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettingsKeys.kt b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettingsKeys.kt new file mode 100644 index 000000000..5285ef863 --- /dev/null +++ b/src/main/kotlin/snyk/common/lsp/settings/LanguageServerSettingsKeys.kt @@ -0,0 +1,49 @@ +package snyk.common.lsp.settings + +object LsSettingsKeys { + const val SNYK_CODE_ENABLED = "snyk_code_enabled" + const val SNYK_OSS_ENABLED = "snyk_oss_enabled" + const val SNYK_IAC_ENABLED = "snyk_iac_enabled" + const val SNYK_SECRETS_ENABLED = "snyk_secrets_enabled" + const val PROXY_INSECURE = "proxy_insecure" + const val API_ENDPOINT = "api_endpoint" + const val ORGANIZATION = "organization" + const val SEND_ERROR_REPORTS = "send_error_reports" + const val AUTOMATIC_DOWNLOAD = "automatic_download" + const val CLI_PATH = "cli_path" + const val BINARY_BASE_URL = "binary_base_url" + const val TOKEN = "token" + const val AUTOMATIC_AUTHENTICATION = "automatic_authentication" + const val ENABLED_SEVERITIES = "enabled_severities" + const val RISK_SCORE_THRESHOLD = "risk_score_threshold" + const val ISSUE_VIEW_OPEN_ISSUES = "issue_view_open_issues" + const val ISSUE_VIEW_IGNORED_ISSUES = "issue_view_ignored_issues" + const val TRUST_ENABLED = "trust_enabled" + const val SCAN_AUTOMATIC = "scan_automatic" + const val AUTHENTICATION_METHOD = "authentication_method" + const val ENABLE_SNYK_OSS_QUICK_FIX_CODE_ACTIONS = "enable_snyk_oss_quick_fix_code_actions" + const val SCAN_NET_NEW = "scan_net_new" + + // Environment Information + const val INTEGRATION_NAME = "integration_name" + const val INTEGRATION_VERSION = "integration_version" + const val INTEGRATION_ENVIRONMENT = "integration_environment" + const val INTEGRATION_ENVIRONMENT_VERSION = "integration_environment_version" + const val DEVICE_ID = "device_id" + const val OS_PLATFORM = "os_platform" + const val OS_ARCH = "os_arch" + const val RUNTIME_NAME = "runtime_name" + const val RUNTIME_VERSION = "runtime_version" +} + +object LsFolderSettingsKeys { + const val BASE_BRANCH = "base_branch" + const val ADDITIONAL_ENVIRONMENT = "additional_environment" + const val ADDITIONAL_PARAMETERS = "additional_parameters" + const val LOCAL_BRANCHES = "local_branches" + const val REFERENCE_FOLDER = "reference_folder" + const val PREFERRED_ORG = "preferred_org" + const val AUTO_DETERMINED_ORG = "auto_determined_org" + const val ORG_SET_BY_USER = "org_set_by_user" + const val SCAN_COMMAND_CONFIG = "scan_command_config" +} diff --git a/src/test/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateServiceTest.kt b/src/test/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateServiceTest.kt index 968013730..1ba08c73e 100644 --- a/src/test/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateServiceTest.kt +++ b/src/test/kotlin/io/snyk/plugin/services/SnykApplicationSettingsStateServiceTest.kt @@ -26,4 +26,10 @@ class SnykApplicationSettingsStateServiceTest { assertFalse(target.hasSeverityEnabled(Severity.CRITICAL)) assertFalse(target.hasSeverityEnabled(Severity.LOW)) } + + @Test + fun requiredLsProtocolVersion_shouldBe25() { + val target = SnykApplicationSettingsStateService() + junit.framework.TestCase.assertEquals(25, target.requiredLsProtocolVersion) + } } diff --git a/src/test/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurableTest.kt b/src/test/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurableTest.kt index 2320116da..0f142654d 100644 --- a/src/test/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurableTest.kt +++ b/src/test/kotlin/io/snyk/plugin/settings/SnykProjectSettingsConfigurableTest.kt @@ -14,9 +14,10 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.folderConfig class SnykProjectSettingsConfigurableTest { @@ -53,7 +54,7 @@ class SnykProjectSettingsConfigurableTest { // Setup initial config with orgSetByUser = true val initialConfig = - FolderConfig( + folderConfig( folderPath = path, baseBranch = "main", preferredOrg = "some-org", @@ -84,8 +85,15 @@ class SnykProjectSettingsConfigurableTest { // Verify the result val resultConfig = folderConfigSettings.getFolderConfig(path) - assertEquals("preferredOrg should be empty", "", resultConfig.preferredOrg) - assertFalse("orgSetByUser should be false (auto-detect enabled)", resultConfig.orgSetByUser) + assertEquals( + "preferredOrg should be empty", + "", + resultConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) + assertFalse( + "orgSetByUser should be false (auto-detect enabled)", + resultConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value as? Boolean ?: false, + ) assertTrue( "isAutoOrganizationEnabled should return true", folderConfigSettings.isAutoOrganizationEnabled(projectMock), @@ -103,7 +111,7 @@ class SnykProjectSettingsConfigurableTest { // Setup initial config with orgSetByUser = true val initialConfig = - FolderConfig( + folderConfig( folderPath = path, baseBranch = "main", preferredOrg = "some-org", @@ -138,11 +146,11 @@ class SnykProjectSettingsConfigurableTest { assertEquals( "preferredOrg should be empty to enable global org fallback", "", - resultConfig.preferredOrg, + resultConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, ) assertTrue( "orgSetByUser should be true (manual mode, empty preferredOrg falls back to global)", - resultConfig.orgSetByUser, + resultConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value as? Boolean ?: false, ) assertFalse( "isAutoOrganizationEnabled should return false", @@ -161,7 +169,7 @@ class SnykProjectSettingsConfigurableTest { // Setup initial config with auto-detect enabled val initialConfig = - FolderConfig(folderPath = path, baseBranch = "main", preferredOrg = "", orgSetByUser = false) + folderConfig(folderPath = path, baseBranch = "main", preferredOrg = "", orgSetByUser = false) folderConfigSettings.addFolderConfig(initialConfig) // Mock the dialog to return a specific org @@ -187,8 +195,15 @@ class SnykProjectSettingsConfigurableTest { // Verify the result val resultConfig = folderConfigSettings.getFolderConfig(path) - assertEquals("preferredOrg should be set", "my-specific-org", resultConfig.preferredOrg) - assertTrue("orgSetByUser should be true (manual org)", resultConfig.orgSetByUser) + assertEquals( + "preferredOrg should be set", + "my-specific-org", + resultConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) + assertTrue( + "orgSetByUser should be true (manual org)", + resultConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value as? Boolean ?: false, + ) assertFalse( "isAutoOrganizationEnabled should return false", folderConfigSettings.isAutoOrganizationEnabled(projectMock), @@ -252,7 +267,7 @@ class SnykProjectSettingsConfigurableTest { for (case in cases) { fcs.clear() - fcs.addFolderConfig(FolderConfig(folderPath = path, baseBranch = "main")) + fcs.addFolderConfig(folderConfig(folderPath = path, baseBranch = "main")) applyFolderConfigChanges( fcs = fcs, @@ -262,7 +277,8 @@ class SnykProjectSettingsConfigurableTest { additionalParameters = case.input, ) - val result = fcs.getFolderConfig(path).additionalParameters + val result = + fcs.getFolderConfig(path).settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value assertEquals("Case '${case.description}': unexpected parsed tokens", case.expected, result) } } @@ -278,7 +294,7 @@ class SnykProjectSettingsConfigurableTest { // Setup initial config val initialConfig = - FolderConfig( + folderConfig( folderPath = path, baseBranch = "main", preferredOrg = "old-org", @@ -309,8 +325,15 @@ class SnykProjectSettingsConfigurableTest { // Verify the result - should use auto-detect since checkbox is checked val resultConfig = folderConfigSettings.getFolderConfig(path) - assertEquals("preferredOrg should be empty (auto-detect)", "", resultConfig.preferredOrg) - assertFalse("orgSetByUser should be false (auto-detect enabled)", resultConfig.orgSetByUser) + assertEquals( + "preferredOrg should be empty (auto-detect)", + "", + resultConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) + assertFalse( + "orgSetByUser should be false (auto-detect enabled)", + resultConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value as? Boolean ?: false, + ) assertTrue( "isAutoOrganizationEnabled should return true", folderConfigSettings.isAutoOrganizationEnabled(projectMock), diff --git a/src/test/kotlin/io/snyk/plugin/ui/ReferenceChooserDialogTest.kt b/src/test/kotlin/io/snyk/plugin/ui/ReferenceChooserDialogTest.kt index 1e48fe6ac..8dd832f48 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/ReferenceChooserDialogTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/ReferenceChooserDialogTest.kt @@ -19,15 +19,18 @@ import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.WorkspaceFolder import org.eclipse.lsp4j.services.LanguageServer import org.junit.Test -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper import snyk.common.lsp.settings.FolderConfigSettings -import snyk.common.lsp.settings.LanguageServerSettings +import snyk.common.lsp.settings.LsFolderSettingsKeys +import snyk.common.lsp.settings.LspConfigurationParam +import snyk.common.lsp.settings.LspFolderConfig +import snyk.common.lsp.settings.folderConfig +import snyk.common.lsp.settings.withSetting import snyk.trust.WorkspaceTrustSettings class ReferenceChooserDialogTest : LightPlatform4TestCase() { private val lsMock: LanguageServer = mockk(relaxed = true) - private lateinit var folderConfig: FolderConfig + private lateinit var folderConfig: LspFolderConfig private lateinit var cut: ReferenceChooserDialog private lateinit var workspaceFolder: WorkspaceFolder private lateinit var languageServerWrapper: LanguageServerWrapper @@ -48,7 +51,7 @@ class ReferenceChooserDialogTest : LightPlatform4TestCase() { // Create a folder config with local branches for the original tests folderConfig = - FolderConfig( + folderConfig( absolutePathString, baseBranch = "testBranch", localBranches = listOf("main", "dev"), @@ -84,10 +87,11 @@ class ReferenceChooserDialogTest : LightPlatform4TestCase() { } /** Helper method to create a folder config with no local branches for testing */ - private fun createFolderConfigWithNoBranches(): FolderConfig { + private fun createFolderConfigWithNoBranches(): LspFolderConfig { val folderConfigSettings = service() val existingConfig = folderConfigSettings.getFolderConfig(folderConfig.folderPath) - val modifiedConfig = existingConfig.copy(localBranches = emptyList()) + val modifiedConfig = + existingConfig.withSetting(LsFolderSettingsKeys.LOCAL_BRANCHES, emptyList()) folderConfigSettings.addFolderConfig(modifiedConfig) return modifiedConfig } @@ -116,11 +120,17 @@ class ReferenceChooserDialogTest : LightPlatform4TestCase() { val capturedParam = CapturingSlot() verify { lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) } - val transmittedSettings = capturedParam.captured.settings as LanguageServerSettings + val transmittedSettings = capturedParam.captured.settings as LspConfigurationParam // we expect the selected item - assertEquals("main", transmittedSettings.folderConfigs[0].baseBranch) + assertEquals( + "main", + transmittedSettings.folderConfigs?.get(0)?.settings?.get("base_branch")?.value, + ) // we also expect the reference folder to be transmitted - assertEquals("/some/reference/path", transmittedSettings.folderConfigs[0].referenceFolderPath) + assertEquals( + "/some/reference/path", + transmittedSettings.folderConfigs?.get(0)?.settings?.get("reference_folder")?.value, + ) } @Test @@ -129,13 +139,15 @@ class ReferenceChooserDialogTest : LightPlatform4TestCase() { val comboBox = ComboBox(arrayOf("main", "dev")).apply { name = folderConfig.folderPath - selectedItem = folderConfig.baseBranch // Use original value, not "main" + selectedItem = + folderConfig.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value as? String ?: "" } // Create a reference folder control with original value (no changes) val referenceFolder = JTextField().apply { - text = folderConfig.referenceFolderPath ?: "" // Use original value + text = + folderConfig.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value as? String ?: "" } val referenceFolderControl = TextFieldWithBrowseButton(referenceFolder) @@ -173,7 +185,8 @@ class ReferenceChooserDialogTest : LightPlatform4TestCase() { // Create a folder config with null local branches val folderConfigSettings = service() val existingConfig = folderConfigSettings.getFolderConfig(folderConfig.folderPath) - val configNullBranches = existingConfig.copy(localBranches = null) + val configNullBranches = + existingConfig.withSetting(LsFolderSettingsKeys.LOCAL_BRANCHES, emptyList()) folderConfigSettings.addFolderConfig(configNullBranches) // Create new dialog instance @@ -221,13 +234,16 @@ class ReferenceChooserDialogTest : LightPlatform4TestCase() { val capturedParam = CapturingSlot() verify { lsMock.workspaceService.didChangeConfiguration(capture(capturedParam)) } - val transmittedSettings = capturedParam.captured.settings as LanguageServerSettings + val transmittedSettings = capturedParam.captured.settings as LspConfigurationParam val transmittedConfig = - transmittedSettings.folderConfigs.find { it.folderPath == configNoBranches.folderPath } + transmittedSettings.folderConfigs?.find { it.folderPath == configNoBranches.folderPath } assertNotNull(transmittedConfig) - assertEquals("", transmittedConfig!!.baseBranch) // Should be empty string - assertEquals("/some/reference/path", transmittedConfig.referenceFolderPath) + assertEquals( + "", + transmittedConfig!!.settings?.get("base_branch")?.value, + ) // Should be empty string + assertEquals("/some/reference/path", transmittedConfig.settings?.get("reference_folder")?.value) } @Test diff --git a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerTest.kt b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerTest.kt index 178617eeb..1888f024b 100644 --- a/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerTest.kt +++ b/src/test/kotlin/io/snyk/plugin/ui/toolwindow/SnykToolWindowSnykScanListenerTest.kt @@ -26,10 +26,10 @@ import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range import snyk.common.annotator.SnykCodeAnnotator import snyk.common.lsp.DataFlow -import snyk.common.lsp.FolderConfig import snyk.common.lsp.IssueData import snyk.common.lsp.ScanIssue import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.folderConfig import snyk.trust.WorkspaceTrustSettings class SnykToolWindowSnykScanListenerTest : BasePlatformTestCase() { @@ -61,7 +61,7 @@ class SnykToolWindowSnykScanListenerTest : BasePlatformTestCase() { val contentRootPaths = project.getContentRootPaths() service() .addFolderConfig( - FolderConfig(contentRootPaths.first().toAbsolutePath().toString(), baseBranch = "main") + folderConfig(contentRootPaths.first().toAbsolutePath().toString(), baseBranch = "main") ) snykToolWindowPanel = SnykToolWindowPanel(project) rootOssIssuesTreeNode = RootOssTreeNode(project) diff --git a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt index 4fb737fa7..7b2003cd8 100644 --- a/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt +++ b/src/test/kotlin/snyk/common/lsp/LanguageServerWrapperTest.kt @@ -439,29 +439,42 @@ class LanguageServerWrapperTest { val expectedTrustedFolders = listOf("/path/to/trusted1", "/path/to/trusted2") every { trustServiceMock.settings.getTrustedPaths() } returns expectedTrustedFolders - val actual = cut.getSettings() + val actual = cut.getInitializationOptions() assertEquals( - settings.snykCodeSecurityIssuesScanEnable.toString(), - actual.activateSnykCodeSecurity, + settings.snykCodeSecurityIssuesScanEnable, + actual.settings?.get("snyk_code_enabled")?.value, ) - assertEquals(settings.iacScanEnabled.toString(), actual.activateSnykIac) - assertEquals(settings.ossScanEnable.toString(), actual.activateSnykOpenSource) - assertEquals(settings.token, actual.token) - assertEquals("${settings.ignoreUnknownCA}", actual.insecure) - assertEquals(getCliFile().absolutePath, actual.cliPath) - assertEquals(settings.organization, actual.organization) - assertEquals(settings.isDeltaFindingsEnabled().toString(), actual.enableDeltaFindings) + assertEquals(settings.iacScanEnabled, actual.settings?.get("snyk_iac_enabled")?.value) + assertEquals(settings.ossScanEnable, actual.settings?.get("snyk_oss_enabled")?.value) + assertEquals(settings.token, actual.settings?.get("token")?.value) + assertEquals(true, actual.settings?.get("token")?.changed) + assertEquals(settings.ignoreUnknownCA, actual.settings?.get("proxy_insecure")?.value) + assertEquals(getCliFile().absolutePath, actual.settings?.get("cli_path")?.value) + assertEquals(settings.organization, actual.settings?.get("organization")?.value) + assertEquals(settings.isDeltaFindingsEnabled(), actual.settings?.get("scan_net_new")?.value) assertEquals(expectedTrustedFolders, actual.trustedFolders) } + @Test + fun `getSettings should always mark token as changed`() { + settings.token = "persistedToken" + // token is NOT explicitly changed - simulates loading from persisted settings + assertFalse(settings.isExplicitlyChanged("token")) + + val actual = cut.getSettings() + + assertEquals("persistedToken", actual.settings?.get("token")?.value) + assertEquals(true, actual.settings?.get("token")?.changed) + } + @Test fun `getSettings should include manageBinariesAutomatically when true`() { settings.manageBinariesAutomatically = true val actual = cut.getSettings() - assertEquals("true", actual.manageBinariesAutomatically) + assertEquals(true, actual.settings?.get("automatic_download")?.value) } @Test @@ -470,7 +483,7 @@ class LanguageServerWrapperTest { val actual = cut.getSettings() - assertEquals("false", actual.manageBinariesAutomatically) + assertEquals(false, actual.settings?.get("automatic_download")?.value) } @Test diff --git a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt index 5715bd712..30a1aae4b 100644 --- a/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt +++ b/src/test/kotlin/snyk/common/lsp/SnykLanguageClientTest.kt @@ -47,7 +47,10 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import snyk.common.lsp.progress.ProgressManager +import snyk.common.lsp.settings.ConfigSetting import snyk.common.lsp.settings.FolderConfigSettings +import snyk.common.lsp.settings.LspConfigurationParam +import snyk.common.lsp.settings.LspFolderConfig import snyk.pluginInfo import snyk.trust.WorkspaceTrustService @@ -335,7 +338,7 @@ class SnykLanguageClientTest { } @Test - fun `folderConfig should call migrateNestedFolderConfigs after adding configs`() { + fun `snykConfiguration should call migrateNestedFolderConfigs after adding configs`() { val folderConfigSettingsMock = mockk(relaxed = true) val lsWrapperMock = mockk(relaxed = true) @@ -349,29 +352,66 @@ class SnykLanguageClientTest { messageBusMock.syncPublisher(SnykFolderConfigListener.SNYK_FOLDER_CONFIG_TOPIC) } returns folderConfigListener - val folderConfig = FolderConfig(folderPath = "/test/project", baseBranch = "main") - val param = FolderConfigsParam(listOf(folderConfig)) + val lspFolderConfig = + LspFolderConfig( + folderPath = "/test/project", + settings = mapOf("base_branch" to ConfigSetting(value = "main")), + ) + val param = LspConfigurationParam(folderConfigs = listOf(lspFolderConfig)) - cut.folderConfig(param) + cut.snykConfiguration(param) - verify(timeout = 5000) { folderConfigSettingsMock.addAll(listOf(folderConfig)) } + verify(timeout = 5000) { folderConfigSettingsMock.addAll(any()) } verify(timeout = 5000) { folderConfigSettingsMock.migrateNestedFolderConfigs(projectMock) } } @Test - fun `folderConfig should not run when disposed`() { + fun `snykConfiguration should not run when disposed`() { every { projectMock.isDisposed } returns true val folderConfigSettingsMock = mockk(relaxed = true) every { applicationMock.getService(FolderConfigSettings::class.java) } returns folderConfigSettingsMock - val param = FolderConfigsParam(listOf(FolderConfig(folderPath = "/test", baseBranch = "main"))) - cut.folderConfig(param) + val param = + LspConfigurationParam( + folderConfigs = + listOf( + LspFolderConfig( + folderPath = "/test", + settings = mapOf("base_branch" to ConfigSetting(value = "main")), + ) + ) + ) + cut.snykConfiguration(param) verify(exactly = 0) { folderConfigSettingsMock.addAll(any()) } } + @Test + fun `snykConfiguration should update plugin settings when received`() { + // initial state + settings.snykCodeSecurityIssuesScanEnable = false + settings.ossScanEnable = false + + val param = + LspConfigurationParam( + settings = + mapOf( + "snyk_code_enabled" to ConfigSetting(value = true, isLocked = true), + "snyk_oss_enabled" to ConfigSetting(value = true, isLocked = false), + ) + ) + + cut.snykConfiguration(param) + + // Give it a small amount of time to process async task + Thread.sleep(200) + + assertTrue(settings.snykCodeSecurityIssuesScanEnable) + assertTrue(settings.ossScanEnable) + } + @Test fun `showDocument should navigate to file URI with selection`() { val fileUri = "file:///tmp/test-file.kt" diff --git a/src/test/kotlin/snyk/common/lsp/settings/FolderConfigSettingsTest.kt b/src/test/kotlin/snyk/common/lsp/settings/FolderConfigSettingsTest.kt index a29574977..f5cc92f0f 100644 --- a/src/test/kotlin/snyk/common/lsp/settings/FolderConfigSettingsTest.kt +++ b/src/test/kotlin/snyk/common/lsp/settings/FolderConfigSettingsTest.kt @@ -16,7 +16,6 @@ import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import snyk.common.lsp.FolderConfig import snyk.common.lsp.LanguageServerWrapper class FolderConfigSettingsTest { @@ -39,7 +38,7 @@ class FolderConfigSettingsTest { val path = "/test/projectA" val normalizedPath = Paths.get(path).normalize().toAbsolutePath().toString() val config = - FolderConfig( + folderConfig( folderPath = path, baseBranch = "main", additionalParameters = listOf("--scan-all-unmanaged"), @@ -50,11 +49,15 @@ class FolderConfigSettingsTest { val retrievedConfig = settings.getFolderConfig(path) assertNotNull("Retrieved config should not be null", retrievedConfig) assertEquals("Normalized path should match", normalizedPath, retrievedConfig.folderPath) - assertEquals("Base branch should match", "main", retrievedConfig.baseBranch) + assertEquals( + "Base branch should match", + "main", + retrievedConfig.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "Additional parameters should match", listOf("--scan-all-unmanaged"), - retrievedConfig.additionalParameters, + retrievedConfig.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value, ) assertEquals("Settings map size should be 1", 1, settings.getAll().size) @@ -69,13 +72,17 @@ class FolderConfigSettingsTest { val rawPath = "/test/projectB/./subfolder/../othersubfolder" val expectedNormalizedPath = Paths.get(rawPath).normalize().toAbsolutePath().toString() - val config = FolderConfig(folderPath = rawPath, baseBranch = "develop") + val config = folderConfig(folderPath = rawPath, baseBranch = "develop") settings.addFolderConfig(config) val retrievedConfig = settings.getFolderConfig(rawPath) assertNotNull("Retrieved config should not be null", retrievedConfig) assertEquals("Normalized path should match", expectedNormalizedPath, retrievedConfig.folderPath) - assertEquals("Base branch should match", "develop", retrievedConfig.baseBranch) + assertEquals( + "Base branch should match", + "develop", + retrievedConfig.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) val retrievedAgain = settings.getFolderConfig(expectedNormalizedPath) assertNotNull("Retrieved again config should not be null", retrievedAgain) @@ -93,60 +100,72 @@ class FolderConfigSettingsTest { val path3 = "/my/project/../project/folder" val normalizedPath1 = Paths.get(path1).normalize().toAbsolutePath().toString() - val config = FolderConfig(folderPath = path1, baseBranch = "feature-branch") + val config = folderConfig(folderPath = path1, baseBranch = "feature-branch") settings.addFolderConfig(config) val retrievedConfig1 = settings.getFolderConfig(path1) assertEquals("Path1 normalized path should match", normalizedPath1, retrievedConfig1.folderPath) - assertEquals("Path1 base branch should match", "feature-branch", retrievedConfig1.baseBranch) + assertEquals( + "Path1 base branch should match", + "feature-branch", + retrievedConfig1.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) val retrievedConfig2 = settings.getFolderConfig(path2) assertEquals("Path2 normalized path should match", normalizedPath1, retrievedConfig2.folderPath) - assertEquals("Path2 base branch should match", "feature-branch", retrievedConfig2.baseBranch) + assertEquals( + "Path2 base branch should match", + "feature-branch", + retrievedConfig2.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) val retrievedConfig3 = settings.getFolderConfig(path3) assertEquals("Path3 normalized path should match", normalizedPath1, retrievedConfig3.folderPath) - assertEquals("Path3 base branch should match", "feature-branch", retrievedConfig3.baseBranch) + assertEquals( + "Path3 base branch should match", + "feature-branch", + retrievedConfig3.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) } @Test fun `addFolderConfig ignores empty or blank folderPaths`() { - settings.addFolderConfig(FolderConfig(folderPath = "", baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = "", baseBranch = "main")) assertEquals("Config with empty path should be ignored", 0, settings.getAll().size) - settings.addFolderConfig(FolderConfig(folderPath = " ", baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = " ", baseBranch = "main")) assertEquals("Config with blank path should be ignored", 0, settings.getAll().size) } @Test - fun `addFolderConfig handles null additionalParameters by defaulting to emptyList`() { + fun `addFolderConfig handles null additionalParameters by not setting key`() { val path = "/test/project" - val config = FolderConfig(folderPath = path, baseBranch = "main", additionalParameters = null) + val config = folderConfig(folderPath = path, baseBranch = "main", additionalParameters = null) settings.addFolderConfig(config) val retrievedConfig = settings.getFolderConfig(path) assertNotNull("Retrieved config should not be null", retrievedConfig) assertEquals( - "additionalParameters should be emptyList when null", - emptyList(), - retrievedConfig.additionalParameters, + "additionalParameters should be null when not set", + null, + retrievedConfig.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value, ) } @Test - fun `addFolderConfig handles null additionalEnv by defaulting to empty string`() { + fun `addFolderConfig handles null additionalEnv by not setting key`() { val path = "/test/project" - val config = FolderConfig(folderPath = path, baseBranch = "main", additionalEnv = null) + val config = folderConfig(folderPath = path, baseBranch = "main", additionalEnv = null) settings.addFolderConfig(config) val retrievedConfig = settings.getFolderConfig(path) assertNotNull("Retrieved config should not be null", retrievedConfig) assertEquals( - "additionalEnv should be empty string when null", - "", - retrievedConfig.additionalEnv, + "additionalEnv should be null when not set", + null, + retrievedConfig.settings?.get(LsFolderSettingsKeys.ADDITIONAL_ENVIRONMENT)?.value, ) } @@ -164,26 +183,30 @@ class FolderConfigSettingsTest { expectedNormalizedPath, newConfig.folderPath, ) - assertEquals("New config should have default baseBranch", "main", newConfig.baseBranch) + assertEquals( + "New config should have default baseBranch", + "main", + newConfig.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "New config additionalParameters should be emptyList", emptyList(), - newConfig.additionalParameters, + newConfig.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value, ) assertEquals( "New config localBranches should be emptyList", emptyList(), - newConfig.localBranches, + newConfig.settings?.get(LsFolderSettingsKeys.LOCAL_BRANCHES)?.value, ) assertEquals( "New config referenceFolderPath should be empty string", "", - newConfig.referenceFolderPath, + newConfig.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value, ) assertEquals( "New config scanCommandConfig should be emptyMap", emptyMap(), - newConfig.scanCommandConfig, + newConfig.settings?.get(LsFolderSettingsKeys.SCAN_COMMAND_CONFIG)?.value, ) val allConfigs = settings.getAll() @@ -206,20 +229,24 @@ class FolderConfigSettingsTest { val normalizedPath = Paths.get(path).normalize().toAbsolutePath().toString() val config1 = - FolderConfig(folderPath = path, baseBranch = "v1", additionalParameters = listOf("param1")) + folderConfig(folderPath = path, baseBranch = "v1", additionalParameters = listOf("param1")) settings.addFolderConfig(config1) var retrieved = settings.getFolderConfig(path) - assertEquals("Retrieved v1 baseBranch", "v1", retrieved.baseBranch) + assertEquals( + "Retrieved v1 baseBranch", + "v1", + retrieved.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "Retrieved v1 additionalParameters", listOf("param1"), - retrieved.additionalParameters, + retrieved.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value, ) assertEquals("Retrieved v1 normalizedPath", normalizedPath, retrieved.folderPath) val config2 = - FolderConfig( + folderConfig( folderPath = equivalentPath, baseBranch = "v2", additionalParameters = listOf("param2"), @@ -227,11 +254,15 @@ class FolderConfigSettingsTest { settings.addFolderConfig(config2) retrieved = settings.getFolderConfig(path) - assertEquals("BaseBranch should be from the overriding config", "v2", retrieved.baseBranch) + assertEquals( + "BaseBranch should be from the overriding config", + "v2", + retrieved.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "AdditionalParameters should be from the overriding config", listOf("param2"), - retrieved.additionalParameters, + retrieved.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value, ) assertEquals( "NormalizedPath should remain the same after overwrite", @@ -250,10 +281,10 @@ class FolderConfigSettingsTest { val normalizedUpper = Paths.get(pathUpper).normalize().toAbsolutePath().toString() val normalizedLower = Paths.get(pathLower).normalize().toAbsolutePath().toString() - val configUpper = FolderConfig(folderPath = pathUpper, baseBranch = "upper") + val configUpper = folderConfig(folderPath = pathUpper, baseBranch = "upper") settings.addFolderConfig(configUpper) - val configLower = FolderConfig(folderPath = pathLower, baseBranch = "lower") + val configLower = folderConfig(folderPath = pathLower, baseBranch = "lower") settings.addFolderConfig(configLower) if ( @@ -268,12 +299,12 @@ class FolderConfigSettingsTest { assertEquals( "BaseBranch for upper case path", "upper", - settings.getFolderConfig(pathUpper).baseBranch, + settings.getFolderConfig(pathUpper).settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, ) assertEquals( "BaseBranch for lower case path", "lower", - settings.getFolderConfig(pathLower).baseBranch, + settings.getFolderConfig(pathLower).settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, ) } else if (normalizedUpper == normalizedLower) { assertEquals( @@ -284,12 +315,12 @@ class FolderConfigSettingsTest { assertEquals( "Lower should overwrite if normalized paths are identical (upper retrieval)", "lower", - settings.getFolderConfig(pathUpper).baseBranch, + settings.getFolderConfig(pathUpper).settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, ) assertEquals( "Lower should overwrite if normalized paths are identical (lower retrieval)", "lower", - settings.getFolderConfig(pathLower).baseBranch, + settings.getFolderConfig(pathLower).settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, ) } else { assertEquals( @@ -300,12 +331,12 @@ class FolderConfigSettingsTest { assertEquals( "BaseBranch for upper case path (distinct)", "upper", - settings.getFolderConfig(pathUpper).baseBranch, + settings.getFolderConfig(pathUpper).settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, ) assertEquals( "BaseBranch for lower case path (distinct)", "lower", - settings.getFolderConfig(pathLower).baseBranch, + settings.getFolderConfig(pathLower).settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, ) } } @@ -318,7 +349,7 @@ class FolderConfigSettingsTest { val expectedNormalizedPath = Paths.get(pathWithoutSlash).normalize().toAbsolutePath().toString() // Add with slash - val config1 = FolderConfig(folderPath = pathWithSlash, baseBranch = "main") + val config1 = folderConfig(folderPath = pathWithSlash, baseBranch = "main") settings.addFolderConfig(config1) // Retrieve with and without slash @@ -350,7 +381,7 @@ class FolderConfigSettingsTest { // Clear and test adding without slash first settings.clear() - val config2 = FolderConfig(folderPath = pathWithoutSlash, baseBranch = "develop") + val config2 = folderConfig(folderPath = pathWithoutSlash, baseBranch = "develop") settings.addFolderConfig(config2) val retrieved2With = settings.getFolderConfig(pathWithSlash) @@ -362,7 +393,10 @@ class FolderConfigSettingsTest { expectedNormalizedPath, retrieved2With.folderPath, ) - assertEquals("develop", retrieved2With.baseBranch) // Ensure correct config is retrieved + assertEquals( + "develop", + retrieved2With.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) // Ensure correct config is retrieved assertNotNull( "Config (added without slash) should be retrievable without slash", retrieved2Without, @@ -372,7 +406,10 @@ class FolderConfigSettingsTest { expectedNormalizedPath, retrieved2Without.folderPath, ) - assertEquals("develop", retrieved2Without.baseBranch) + assertEquals( + "develop", + retrieved2Without.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "Both retrievals should yield the same object instance", retrieved2With, @@ -393,7 +430,7 @@ class FolderConfigSettingsTest { val rootPathWithSlash = "/" val rootPathNormalized = Paths.get(rootPathWithSlash).normalize().toAbsolutePath().toString() - val config = FolderConfig(folderPath = rootPathWithSlash, baseBranch = "rootBranch") + val config = folderConfig(folderPath = rootPathWithSlash, baseBranch = "rootBranch") settings.addFolderConfig(config) val retrieved = settings.getFolderConfig(rootPathWithSlash) assertNotNull("Retrieved config for root path should not be null", retrieved) @@ -422,19 +459,23 @@ class FolderConfigSettingsTest { @Test fun `addFolderConfig stores and retrieves preferredOrg`() { val path = "/test/project" - val config = FolderConfig(folderPath = path, baseBranch = "main", preferredOrg = "my-org-uuid") + val config = folderConfig(folderPath = path, baseBranch = "main", preferredOrg = "my-org-uuid") settings.addFolderConfig(config) val retrievedConfig = settings.getFolderConfig(path) assertNotNull("Retrieved config should not be null", retrievedConfig) - assertEquals("Preferred org should match", "my-org-uuid", retrievedConfig.preferredOrg) + assertEquals( + "Preferred org should match", + "my-org-uuid", + retrievedConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) } @Test fun `addFolderConfig with empty preferredOrg uses default`() { val path = "/test/project" - val config = FolderConfig(folderPath = path, baseBranch = "main") + val config = folderConfig(folderPath = path, baseBranch = "main") settings.addFolderConfig(config) @@ -443,7 +484,7 @@ class FolderConfigSettingsTest { assertEquals( "Preferred org should be empty string by default", "", - retrievedConfig.preferredOrg, + retrievedConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, ) } @@ -453,23 +494,35 @@ class FolderConfigSettingsTest { val newConfig = settings.getFolderConfig(path) assertNotNull("New config should not be null", newConfig) - assertEquals("New config preferredOrg should be empty string", "", newConfig.preferredOrg) + assertEquals( + "New config preferredOrg should be empty string", + "", + newConfig.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) } @Test fun `addFolderConfig overwrites preferredOrg when config is updated`() { val path = "/test/project" - val config1 = FolderConfig(folderPath = path, baseBranch = "main", preferredOrg = "first-org") + val config1 = folderConfig(folderPath = path, baseBranch = "main", preferredOrg = "first-org") settings.addFolderConfig(config1) val retrieved1 = settings.getFolderConfig(path) - assertEquals("First preferredOrg should match", "first-org", retrieved1.preferredOrg) + assertEquals( + "First preferredOrg should match", + "first-org", + retrieved1.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) - val config2 = FolderConfig(folderPath = path, baseBranch = "main", preferredOrg = "second-org") + val config2 = folderConfig(folderPath = path, baseBranch = "main", preferredOrg = "second-org") settings.addFolderConfig(config2) val retrieved2 = settings.getFolderConfig(path) - assertEquals("PreferredOrg should be updated", "second-org", retrieved2.preferredOrg) + assertEquals( + "PreferredOrg should be updated", + "second-org", + retrieved2.settings?.get(LsFolderSettingsKeys.PREFERRED_ORG)?.value, + ) } @Test @@ -499,8 +552,8 @@ class FolderConfigSettingsTest { val normalizedPath2 = Paths.get(path2).normalize().toAbsolutePath().toString() // Add configs with preferredOrg - val config1 = FolderConfig(folderPath = path1, baseBranch = "main", preferredOrg = "org-uuid-1") - val config2 = FolderConfig(folderPath = path2, baseBranch = "main", preferredOrg = "org-uuid-2") + val config1 = folderConfig(folderPath = path1, baseBranch = "main", preferredOrg = "org-uuid-1") + val config2 = folderConfig(folderPath = path2, baseBranch = "main", preferredOrg = "org-uuid-2") settings.addFolderConfig(config1) settings.addFolderConfig(config2) @@ -535,7 +588,7 @@ class FolderConfigSettingsTest { val path1 = "/test/project1" val normalizedPath1 = Paths.get(path1).normalize().toAbsolutePath().toString() - val config1 = FolderConfig(folderPath = path1, baseBranch = "main", preferredOrg = "") + val config1 = folderConfig(folderPath = path1, baseBranch = "main", preferredOrg = "") settings.addFolderConfig(config1) val workspaceFolder1 = @@ -565,8 +618,8 @@ class FolderConfigSettingsTest { val normalizedPath1 = Paths.get(path1).normalize().toAbsolutePath().toString() val normalizedPath2 = Paths.get(path2).normalize().toAbsolutePath().toString() - val config1 = FolderConfig(folderPath = path1, baseBranch = "main", preferredOrg = "org-uuid-1") - val config2 = FolderConfig(folderPath = path2, baseBranch = "main", preferredOrg = "org-uuid-2") + val config1 = folderConfig(folderPath = path1, baseBranch = "main", preferredOrg = "org-uuid-1") + val config2 = folderConfig(folderPath = path2, baseBranch = "main", preferredOrg = "org-uuid-2") settings.addFolderConfig(config1) settings.addFolderConfig(config2) @@ -596,41 +649,57 @@ class FolderConfigSettingsTest { @Test fun `addFolderConfig stores and retrieves orgSetByUser with default false`() { val path = "/test/project" - val config = FolderConfig(folderPath = path, baseBranch = "main") + val config = folderConfig(folderPath = path, baseBranch = "main") settings.addFolderConfig(config) val retrievedConfig = settings.getFolderConfig(path) assertNotNull("Retrieved config should not be null", retrievedConfig) - assertEquals("orgSetByUser should default to false", false, retrievedConfig.orgSetByUser) + assertEquals( + "orgSetByUser should default to false", + false, + retrievedConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value, + ) } @Test fun `addFolderConfig stores and retrieves orgSetByUser set to true`() { val path = "/test/project" - val config = FolderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) + val config = folderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) settings.addFolderConfig(config) val retrievedConfig = settings.getFolderConfig(path) assertNotNull("Retrieved config should not be null", retrievedConfig) - assertEquals("orgSetByUser should be true", true, retrievedConfig.orgSetByUser) + assertEquals( + "orgSetByUser should be true", + true, + retrievedConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value, + ) } @Test fun `addFolderConfig overwrites orgSetByUser when config is updated`() { val path = "/test/project" - val config1 = FolderConfig(folderPath = path, baseBranch = "main", orgSetByUser = false) + val config1 = folderConfig(folderPath = path, baseBranch = "main", orgSetByUser = false) settings.addFolderConfig(config1) val retrieved1 = settings.getFolderConfig(path) - assertEquals("First orgSetByUser should be false", false, retrieved1.orgSetByUser) + assertEquals( + "First orgSetByUser should be false", + false, + retrieved1.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value, + ) - val config2 = FolderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) + val config2 = folderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) settings.addFolderConfig(config2) val retrieved2 = settings.getFolderConfig(path) - assertEquals("orgSetByUser should be updated to true", true, retrieved2.orgSetByUser) + assertEquals( + "orgSetByUser should be updated to true", + true, + retrieved2.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value, + ) } @Test @@ -639,7 +708,11 @@ class FolderConfigSettingsTest { val newConfig = settings.getFolderConfig(path) assertNotNull("New config should not be null", newConfig) - assertEquals("New config orgSetByUser should be false", false, newConfig.orgSetByUser) + assertEquals( + "New config orgSetByUser should be false", + false, + newConfig.settings?.get(LsFolderSettingsKeys.ORG_SET_BY_USER)?.value, + ) } @Test @@ -650,7 +723,7 @@ class FolderConfigSettingsTest { val path = "/test/project" val workspaceFolder = WorkspaceFolder().apply { uri = path.fromPathToUriString() } - val config = FolderConfig(folderPath = path, baseBranch = "main", orgSetByUser = false) + val config = folderConfig(folderPath = path, baseBranch = "main", orgSetByUser = false) settings.addFolderConfig(config) @@ -673,7 +746,7 @@ class FolderConfigSettingsTest { val path = "/test/project" val workspaceFolder = WorkspaceFolder().apply { uri = path.fromPathToUriString() } - val config = FolderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) + val config = folderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) settings.addFolderConfig(config) @@ -696,7 +769,7 @@ class FolderConfigSettingsTest { val path = "/test/project" val workspaceFolder = WorkspaceFolder().apply { uri = path.fromPathToUriString() } - val config = FolderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) + val config = folderConfig(folderPath = path, baseBranch = "main", orgSetByUser = true) settings.addFolderConfig(config) @@ -728,7 +801,7 @@ class FolderConfigSettingsTest { val path = "/test/project" val workspaceFolder = WorkspaceFolder().apply { uri = path.fromPathToUriString() } - val config = FolderConfig(folderPath = path, baseBranch = "main", preferredOrg = "old-org") + val config = folderConfig(folderPath = path, baseBranch = "main", preferredOrg = "old-org") settings.addFolderConfig(config) @@ -764,8 +837,8 @@ class FolderConfigSettingsTest { val normalizedNestedPath = Paths.get(nestedPath).normalize().toAbsolutePath().toString() // Add both configs with SAME values (nested has default/same values as parent) - val workspaceConfig = FolderConfig(folderPath = workspacePath, baseBranch = "main") - val nestedConfig = FolderConfig(folderPath = nestedPath, baseBranch = "main") // Same as parent + val workspaceConfig = folderConfig(folderPath = workspacePath, baseBranch = "main") + val nestedConfig = folderConfig(folderPath = nestedPath, baseBranch = "main") // Same as parent settings.addFolderConfig(workspaceConfig) settings.addFolderConfig(nestedConfig) @@ -809,8 +882,8 @@ class FolderConfigSettingsTest { val normalizedPath1 = Paths.get(path1).normalize().toAbsolutePath().toString() val normalizedPath2 = Paths.get(path2).normalize().toAbsolutePath().toString() - val config1 = FolderConfig(folderPath = path1, baseBranch = "main") - val config2 = FolderConfig(folderPath = path2, baseBranch = "develop") + val config1 = folderConfig(folderPath = path1, baseBranch = "main") + val config2 = folderConfig(folderPath = path2, baseBranch = "develop") settings.addFolderConfig(config1) settings.addFolderConfig(config2) @@ -846,7 +919,7 @@ class FolderConfigSettingsTest { val lsWrapperMock = mockk(relaxed = true) val path = "/test/project" - settings.addFolderConfig(FolderConfig(folderPath = path, baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = path, baseBranch = "main")) assertEquals("Should have 1 config before migration", 1, settings.getAll().size) @@ -872,8 +945,8 @@ class FolderConfigSettingsTest { val normalizedWorkspacePath = Paths.get(workspacePath).normalize().toAbsolutePath().toString() // Add both configs - settings.addFolderConfig(FolderConfig(folderPath = workspacePath, baseBranch = "main")) - settings.addFolderConfig(FolderConfig(folderPath = nestedPath, baseBranch = "develop")) + settings.addFolderConfig(folderConfig(folderPath = workspacePath, baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = nestedPath, baseBranch = "develop")) val workspaceFolder = WorkspaceFolder().apply { @@ -901,8 +974,8 @@ class FolderConfigSettingsTest { @Test fun `hasNonDefaultValues returns true when config differs from parent`() { - val parentConfig = FolderConfig(folderPath = "/parent", baseBranch = "main") - val childConfig = FolderConfig(folderPath = "/child", baseBranch = "develop") + val parentConfig = folderConfig(folderPath = "/parent", baseBranch = "main") + val childConfig = folderConfig(folderPath = "/child", baseBranch = "develop") assertTrue( "Should detect different baseBranch", @@ -912,8 +985,8 @@ class FolderConfigSettingsTest { @Test fun `hasNonDefaultValues returns false when config matches parent`() { - val parentConfig = FolderConfig(folderPath = "/parent", baseBranch = "main") - val childConfig = FolderConfig(folderPath = "/child", baseBranch = "main") + val parentConfig = folderConfig(folderPath = "/parent", baseBranch = "main") + val childConfig = folderConfig(folderPath = "/child", baseBranch = "main") assertFalse( "Should not detect differences when configs match", @@ -923,8 +996,8 @@ class FolderConfigSettingsTest { @Test fun `hasConflictingConfigs returns true when configs differ`() { - val config1 = FolderConfig(folderPath = "/path1", baseBranch = "main") - val config2 = FolderConfig(folderPath = "/path2", baseBranch = "develop") + val config1 = folderConfig(folderPath = "/path1", baseBranch = "main") + val config2 = folderConfig(folderPath = "/path2", baseBranch = "develop") assertTrue( "Should detect conflicting configs", @@ -934,8 +1007,8 @@ class FolderConfigSettingsTest { @Test fun `hasConflictingConfigs returns false when configs match`() { - val config1 = FolderConfig(folderPath = "/path1", baseBranch = "main") - val config2 = FolderConfig(folderPath = "/path2", baseBranch = "main") + val config1 = folderConfig(folderPath = "/path1", baseBranch = "main") + val config2 = folderConfig(folderPath = "/path2", baseBranch = "main") assertFalse( "Should not detect conflicts when configs match", @@ -946,14 +1019,14 @@ class FolderConfigSettingsTest { @Test fun `mergeConfigs copies sub-config values into parent`() { val parentConfig = - FolderConfig( + folderConfig( folderPath = "/parent", baseBranch = "main", referenceFolderPath = "/old/ref", additionalParameters = listOf("--old"), ) val subConfig = - FolderConfig( + folderConfig( folderPath = "/child", baseBranch = "develop", referenceFolderPath = "/new/ref", @@ -963,16 +1036,20 @@ class FolderConfigSettingsTest { val merged = settings.mergeConfigs(parentConfig, subConfig) assertEquals("Should keep parent folderPath", "/parent", merged.folderPath) - assertEquals("Should use sub-config baseBranch", "develop", merged.baseBranch) + assertEquals( + "Should use sub-config baseBranch", + "develop", + merged.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "Should use sub-config referenceFolderPath", "/new/ref", - merged.referenceFolderPath, + merged.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value, ) assertEquals( "Should use sub-config additionalParameters", listOf("--new"), - merged.additionalParameters, + merged.settings?.get(LsFolderSettingsKeys.ADDITIONAL_PARAMETERS)?.value, ) } @@ -996,9 +1073,9 @@ class FolderConfigSettingsTest { val normalizedNestedPath = Paths.get(nestedPath).normalize().toAbsolutePath().toString() // Parent and nested have DIFFERENT values - settings.addFolderConfig(FolderConfig(folderPath = workspacePath, baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = workspacePath, baseBranch = "main")) settings.addFolderConfig( - FolderConfig( + folderConfig( folderPath = nestedPath, baseBranch = "develop", referenceFolderPath = "/custom/ref", @@ -1029,11 +1106,15 @@ class FolderConfigSettingsTest { // Verify parent was updated with sub-config values val parentConfig = settingsSpy.getAll()[normalizedWorkspacePath] - assertEquals("Parent should have merged baseBranch", "develop", parentConfig?.baseBranch) + assertEquals( + "Parent should have merged baseBranch", + "develop", + parentConfig?.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) assertEquals( "Parent should have merged referenceFolderPath", "/custom/ref", - parentConfig?.referenceFolderPath, + parentConfig?.settings?.get(LsFolderSettingsKeys.REFERENCE_FOLDER)?.value, ) } @@ -1048,8 +1129,8 @@ class FolderConfigSettingsTest { val normalizedNestedPath = Paths.get(nestedPath).normalize().toAbsolutePath().toString() // Parent and nested have DIFFERENT values - settings.addFolderConfig(FolderConfig(folderPath = workspacePath, baseBranch = "main")) - settings.addFolderConfig(FolderConfig(folderPath = nestedPath, baseBranch = "develop")) + settings.addFolderConfig(folderConfig(folderPath = workspacePath, baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = nestedPath, baseBranch = "develop")) val workspaceFolder = WorkspaceFolder().apply { @@ -1075,7 +1156,11 @@ class FolderConfigSettingsTest { // Verify parent was NOT updated val parentConfig = settingsSpy.getAll()[normalizedWorkspacePath] - assertEquals("Parent should keep original baseBranch", "main", parentConfig?.baseBranch) + assertEquals( + "Parent should keep original baseBranch", + "main", + parentConfig?.settings?.get(LsFolderSettingsKeys.BASE_BRANCH)?.value, + ) } @Test @@ -1091,9 +1176,9 @@ class FolderConfigSettingsTest { val normalizedNestedPath2 = Paths.get(nestedPath2).normalize().toAbsolutePath().toString() // Parent and two nested configs with DIFFERENT values from each other - settings.addFolderConfig(FolderConfig(folderPath = workspacePath, baseBranch = "main")) - settings.addFolderConfig(FolderConfig(folderPath = nestedPath1, baseBranch = "develop")) - settings.addFolderConfig(FolderConfig(folderPath = nestedPath2, baseBranch = "feature")) + settings.addFolderConfig(folderConfig(folderPath = workspacePath, baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = nestedPath1, baseBranch = "develop")) + settings.addFolderConfig(folderConfig(folderPath = nestedPath2, baseBranch = "feature")) val workspaceFolder = WorkspaceFolder().apply { @@ -1147,9 +1232,9 @@ class FolderConfigSettingsTest { val normalizedNestedPath2 = Paths.get(nestedPath2).normalize().toAbsolutePath().toString() // Parent and two nested configs with DIFFERENT values from each other - settings.addFolderConfig(FolderConfig(folderPath = workspacePath, baseBranch = "main")) - settings.addFolderConfig(FolderConfig(folderPath = nestedPath1, baseBranch = "develop")) - settings.addFolderConfig(FolderConfig(folderPath = nestedPath2, baseBranch = "feature")) + settings.addFolderConfig(folderConfig(folderPath = workspacePath, baseBranch = "main")) + settings.addFolderConfig(folderConfig(folderPath = nestedPath1, baseBranch = "develop")) + settings.addFolderConfig(folderConfig(folderPath = nestedPath2, baseBranch = "feature")) val workspaceFolder = WorkspaceFolder().apply { diff --git a/src/test/kotlin/snyk/common/lsp/settings/TestFolderConfigHelper.kt b/src/test/kotlin/snyk/common/lsp/settings/TestFolderConfigHelper.kt new file mode 100644 index 000000000..c891ca744 --- /dev/null +++ b/src/test/kotlin/snyk/common/lsp/settings/TestFolderConfigHelper.kt @@ -0,0 +1,37 @@ +package snyk.common.lsp.settings + +import snyk.common.lsp.ScanCommandConfig + +@Suppress("LongParameterList") +fun folderConfig( + folderPath: String, + baseBranch: String = "main", + localBranches: List? = emptyList(), + additionalParameters: List? = emptyList(), + additionalEnv: String? = "", + referenceFolderPath: String? = "", + preferredOrg: String = "", + autoDeterminedOrg: String = "", + orgSetByUser: Boolean = false, + scanCommandConfig: Map? = emptyMap(), +): LspFolderConfig { + val settings = mutableMapOf() + settings[LsFolderSettingsKeys.BASE_BRANCH] = ConfigSetting(value = baseBranch) + localBranches?.let { settings[LsFolderSettingsKeys.LOCAL_BRANCHES] = ConfigSetting(value = it) } + additionalParameters?.let { + settings[LsFolderSettingsKeys.ADDITIONAL_PARAMETERS] = ConfigSetting(value = it) + } + additionalEnv?.let { + settings[LsFolderSettingsKeys.ADDITIONAL_ENVIRONMENT] = ConfigSetting(value = it) + } + referenceFolderPath?.let { + settings[LsFolderSettingsKeys.REFERENCE_FOLDER] = ConfigSetting(value = it) + } + settings[LsFolderSettingsKeys.PREFERRED_ORG] = ConfigSetting(value = preferredOrg) + settings[LsFolderSettingsKeys.AUTO_DETERMINED_ORG] = ConfigSetting(value = autoDeterminedOrg) + settings[LsFolderSettingsKeys.ORG_SET_BY_USER] = ConfigSetting(value = orgSetByUser) + scanCommandConfig?.let { + settings[LsFolderSettingsKeys.SCAN_COMMAND_CONFIG] = ConfigSetting(value = it) + } + return LspFolderConfig(folderPath = folderPath, settings = settings) +} diff --git a/test-output.txt b/test-output.txt new file mode 100644 index 000000000..ac52d8618 --- /dev/null +++ b/test-output.txt @@ -0,0 +1,10 @@ +> Task :checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :initializeIntellijPlatformPlugin +> Task :patchPluginXml UP-TO-DATE +> Task :processResources UP-TO-DATE +> Task :generateManifest UP-TO-DATE +> Task :processTestResources UP-TO-DATE +> Task :koverFindJar UP-TO-DATE + +> Task :compileKotlin FAILED +7 actionable tasks: 2 executed, 5 up-to-date diff --git a/tests.json b/tests.json new file mode 100644 index 000000000..2902b85d8 --- /dev/null +++ b/tests.json @@ -0,0 +1,51 @@ +{ + "ticket": "IDE-1639", + "description": "Identify the changes in the LSP configuration communication and write an implementation plan to support it. use LS_PROTOCOL_VERSION 25.", + "lastUpdated": "2026-03-06", + "lastSession": { + "date": "2026-03-06", + "sessionNumber": 1, + "completedSteps": [], + "currentStep": "1.1 Requirements Analysis", + "nextStep": "1.2 Schema Design" + }, + "testSuites": { + "unit": { + "configuration": { + "status": "pending", + "scenarios": [ + { + "id": "CFG-001", + "name": "Should serialize DidChangeConfigurationParams using LspConfigurationParam structure", + "description": "Verify that getSettings returns the new LspConfigurationParam structure", + "status": "passed" + }, + { + "id": "CFG-002", + "name": "requiredLsProtocolVersion should be 25", + "description": "Verify that SnykApplicationSettingsStateService.requiredLsProtocolVersion is updated to 25", + "status": "passed" + }, + { + "id": "CFG-003", + "name": "Should handle $/snyk.configuration notification", + "description": "Verify that SnykLanguageClient.snykConfiguration updates the local SnykApplicationSettingsStateService properties according to the canonical pflag names", + "status": "passed" + }, + { + "id": "CFG-004", + "name": "Should update FolderConfigSettings via $/snyk.configuration", + "description": "Verify that SnykLanguageClient.snykConfiguration updates the FolderConfigSettings with the passed folderConfigs instead of the old $/snyk.folderConfigs endpoint", + "status": "passed" + } + ] + } + }, + "integration": { + "scenarios": [] + }, + "regression": { + "scenarios": [] + } + } +}