Skip to content

Commit 9713fed

Browse files
Merge pull request #137 from codacy/create-separate-vscode-rules
feat: create individual codacy rules file for vscode CF-1858
2 parents 1febd3a + 50f5601 commit 9713fed

4 files changed

Lines changed: 314 additions & 282 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@ codacy-vscode-extension-CHANGELOG.txt
3737

3838
#Ignore vscode AI rules
3939
.github/copilot-instructions.md
40+
41+
42+
#Ignore windsurf AI rules
43+
.windsurfrules

src/commands/configureMCP.ts

Lines changed: 2 additions & 281 deletions
Original file line numberDiff line numberDiff line change
@@ -10,297 +10,18 @@ import Logger from '../common/logger'
1010
import { CodacyError, Config } from '../common'
1111
import { RepositoryParams } from '../git/CodacyCloud'
1212
import { createWindsurfWorkflows } from './createWorkflows'
13-
14-
interface Rule {
15-
when?: string
16-
enforce: string[]
17-
scope: 'guardrails' | 'general'
18-
}
19-
20-
interface RuleConfig {
21-
name: string
22-
description: string
23-
rules: Rule[]
24-
}
25-
26-
const newRulesTemplate = (params?: RepositoryParams, excludedScopes?: ('guardrails' | 'general')[]): RuleConfig => {
27-
const repositoryRules: Rule[] = []
28-
if (params) {
29-
const { provider, organization, repository } = params
30-
repositoryRules.push({
31-
when: 'using any tool that accepts the arguments: `provider`, `organization`, or `repository`',
32-
enforce: [
33-
'ALWAYS use:',
34-
`- provider: ${provider}`,
35-
`- organization: ${organization}`,
36-
`- repository: ${repository}`,
37-
'Avoid calling `git remote -v` unless really necessary',
38-
],
39-
scope: 'general',
40-
})
41-
}
42-
43-
const codacyCLISettingsPath = path.join(
44-
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '',
45-
'.codacy',
46-
'codacy.yaml'
47-
)
48-
49-
const enigmaRules: Rule[] = []
50-
if (fs.existsSync(codacyCLISettingsPath)) {
51-
const codacyCLITools = fs.readFileSync(codacyCLISettingsPath, 'utf8')
52-
if (codacyCLITools.includes('enigma')) {
53-
enigmaRules.push({
54-
when: 'When user asks to create a rule',
55-
scope: 'general',
56-
enforce: [
57-
'To add a new rule for code analysis, follow these steps:',
58-
'- Create or edit a file named `enigma.yaml` in the root of the project.',
59-
'- Each rule should be listed under the `rules:` key as an item in a YAML list.',
60-
`- Example rule format:
61-
\`\`\`yaml
62-
rules:,
63-
- Id: python_hardcoded_password,
64-
Pattern: $PASSWORD = $VALUE,
65-
Description: Detects hardcoded passwords in string variable declarations,
66-
Category: Security,
67-
MetaTags:,
68-
- Id: PASSWORD,
69-
Regex: ...,
70-
- Id: VALUE,
71-
Regex: ...,
72-
Languages:,
73-
- python,
74-
\`\`\``,
75-
'Pattern Field',
76-
'- The `Pattern` is NOT a regex. It is a literal code pattern, but you can use MetaTags (like `$PASSWORD` or `$VALUE`) as placeholders.',
77-
'- MetaTags must start with a `$` and be defined in the `MetaTags` section.',
78-
'MetaTags',
79-
'- Every MetaTag used in the `Pattern` must have a definition under `MetaTags`.',
80-
'- Each MetaTag must have an `Id` and a `Regex`.',
81-
'- The `Regex` must be Perl-compatible (PCRE), but negative lookaheads are NOT supported. Please always wrap in double quotes.',
82-
'Languages: List the programming languages this rule applies to under `Languages`.',
83-
'Testing Your Rule: After creating or editing a rule, test it by running the codacy_cli_analyze tool with:',
84-
'- rootPath set to the project root',
85-
'- no file',
86-
'- tool set to "codacy-enigma-cli"',
87-
'Check the output for any parsing errors and fix them if needed.',
88-
'Summary - All rules must:',
89-
'- Be in `enigma.yaml` at the project root',
90-
'- Define all MetaTags used in the Pattern',
91-
'- Use only supported regex features in MetaTags',
92-
'- Be tested for parsing errors using the CLI',
93-
],
94-
})
95-
}
96-
}
97-
98-
const commonRules: Rule[] = [
99-
{
100-
when: 'CRITICAL: After ANY successful `edit_file` or `reapply` operation',
101-
enforce: [
102-
"YOU MUST IMMEDIATELY run the `codacy_cli_analyze` tool from Codacy's MCP Server for each file that was edited, with:",
103-
'- `rootPath`: set to the workspace path',
104-
'- `file`: set to the path of the edited file',
105-
'- `tool`: leave empty or unset',
106-
'If any issues are found in the new edits, propose and apply fixes for them.',
107-
'> NOTE: Failure to follow this rule is considered a critical error.',
108-
],
109-
scope: 'guardrails',
110-
},
111-
{
112-
when: 'After every response',
113-
enforce: [
114-
"If you made any file edits in this conversation, verify you ran `codacy_cli_analyze` tool from Codacy's MCP Server ",
115-
],
116-
scope: 'guardrails',
117-
},
118-
{
119-
when: 'When there are no Codacy MCP Server tools available, or the MCP Server is not reachable',
120-
enforce: [
121-
'Suggest the user the following troubleshooting steps:',
122-
'- Try to reset the MCP on the extension',
123-
'- If the user is using VSCode, suggest them to review their Copilot > MCP settings in Github, under their organization or personal account. Refer them to Settings > Copilot > Enable MCP servers in Copilot. Suggested URL (https://github.com/settings/copilot/features) or https://github.com/organizations/{organization-name}/settings/copilot/features (This can only be done by their organization admins / owners)',
124-
'If none of the above steps work, suggest the user to contact Codacy support',
125-
],
126-
scope: 'guardrails',
127-
},
128-
{
129-
when: 'Trying to call a tool that needs a rootPath as a parameter',
130-
enforce: ['Always use the standard, non-URL-encoded file system path'],
131-
scope: 'guardrails',
132-
},
133-
{
134-
when: 'CRITICAL: Dependencies and Security Checks',
135-
enforce: [
136-
'IMMEDIATELY after ANY of these actions:',
137-
'- Running npm/yarn/pnpm install',
138-
'- Adding dependencies to package.json',
139-
'- Adding requirements to requirements.txt',
140-
'- Adding dependencies to pom.xml',
141-
'- Adding dependencies to build.gradle',
142-
'- Any other package manager operations',
143-
'You MUST run the `codacy_cli_analyze` tool with:',
144-
'- `rootPath`: set to the workspace path',
145-
'- `tool`: set to "trivy"',
146-
'- `file`: leave empty or unset',
147-
'If any vulnerabilities are found because of the newly added packages:',
148-
'- Stop all other operations',
149-
'- Propose and apply fixes for the security issues',
150-
'- Only continue with the original task after security issues are resolved',
151-
'EXAMPLE:',
152-
'- After: npm install react-markdown',
153-
'- Do: Run codacy_cli_analyze with trivy',
154-
'- Before: Continuing with any other tasks',
155-
],
156-
scope: 'guardrails',
157-
},
158-
{
159-
enforce: [
160-
'Repeat the relevant steps for each modified file.',
161-
'"Propose fixes" means to both suggest and, if possible, automatically apply the fixes.',
162-
'You MUST NOT wait for the user to ask for analysis or remind you to run the tool.',
163-
'Do not run `codacy_cli_analyze` looking for changes in duplicated code or code complexity metrics.',
164-
'Do not run `codacy_cli_analyze` looking for changes in code coverage.',
165-
'Do not try to manually install Codacy CLI using either brew, npm, npx, or any other package manager.',
166-
"If the Codacy CLI is not installed, just run the `codacy_cli_analyze` tool from Codacy's MCP Server.",
167-
'When calling `codacy_cli_analyze`, only send provider, organization and repository if the project is a git repository.',
168-
],
169-
scope: 'guardrails',
170-
},
171-
{
172-
when: 'Whenever a call to a Codacy tool that uses `repository` or `organization` as a parameter returns a 404 error',
173-
enforce: [
174-
'Offer to run the `codacy_setup_repository` tool to add the repository to Codacy',
175-
'If the user accepts, run the `codacy_setup_repository` tool',
176-
'Do not ever try to run the `codacy_setup_repository` tool on your own',
177-
'After setup, immediately retry the action that failed (only retry once)',
178-
],
179-
scope: 'general',
180-
},
181-
]
182-
183-
return {
184-
name: 'Codacy Rules',
185-
description: "Configuration for AI behavior when interacting with Codacy's MCP Server",
186-
rules: [...repositoryRules, ...commonRules, ...enigmaRules].filter((rule) => !excludedScopes?.includes(rule.scope)),
187-
}
188-
}
13+
import { createOrUpdateRules } from './createRules'
18914

19015
// Function to sanitize the JSON content to avoid trailing commas
19116
const sanitizeJSON = (json: string): string => {
19217
return json.replace(/,([\s\r\n]*[}\]])/g, '$1')
19318
}
19419

195-
const convertRulesToMarkdown = (rules: RuleConfig, existingContent?: string): string => {
196-
const newCodacyRules = `\n# ${rules.name}\n${rules.description}\n\n${rules.rules
197-
.map(
198-
(rule) =>
199-
`${rule.when ? `## ${rule.when}\n` : '## General\n'}${rule.enforce
200-
.map((e) => (e.startsWith('-') ? ` ${e}` : `- ${e}`))
201-
.join('\n')}`
202-
)
203-
.join('\n\n')}\n`
204-
205-
if (!existingContent) {
206-
return `---${newCodacyRules}---`
207-
}
208-
209-
const existingRules = existingContent?.split('---')
210-
211-
return existingRules.map((content) => (content.indexOf(rules.name) >= 0 ? newCodacyRules : content)).join('---')
212-
}
213-
214-
const rulesPrefixForMdc = `---
215-
description:
216-
globs:
217-
alwaysApply: true
218-
---
219-
\n`
220-
221-
function getCorrectRulesInfo(): { path: string; format: string } {
222-
const ideInfo = getCurrentIDE()
223-
// Get the workspace folder path
224-
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
225-
if (!workspacePath) {
226-
throw new Error('No workspace folder found')
227-
}
228-
if (ideInfo === 'cursor') {
229-
return { path: path.join(workspacePath, '.cursor', 'rules', 'codacy.mdc'), format: 'mdc' }
230-
}
231-
if (ideInfo === 'windsurf') {
232-
return { path: path.join(workspacePath, '.windsurfrules'), format: 'md' }
233-
}
234-
return { path: path.join(workspacePath, '.github', 'copilot-instructions.md'), format: 'md' }
235-
}
236-
237-
const addRulesToGitignore = (rulesPath: string) => {
238-
const currentIDE = getCurrentIDE()
239-
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''
240-
const gitignorePath = path.join(workspacePath, '.gitignore')
241-
const relativeRulesPath = path.relative(workspacePath, rulesPath)
242-
const gitignoreContent = `\n\n#Ignore ${currentIDE} AI rules\n${relativeRulesPath}\n`
243-
let existingGitignore = ''
244-
245-
if (fs.existsSync(gitignorePath)) {
246-
existingGitignore = fs.readFileSync(gitignorePath, 'utf8')
247-
248-
if (!existingGitignore.split('\n').some((line) => line.trim() === relativeRulesPath.trim())) {
249-
fs.appendFileSync(gitignorePath, gitignoreContent)
250-
Logger.appendLine(`Added ${relativeRulesPath} to .gitignore`)
251-
}
252-
} else {
253-
fs.writeFileSync(gitignorePath, gitignoreContent)
254-
Logger.appendLine('Created .gitignore and added rules path')
255-
}
256-
}
257-
25820
export function updateMCPState() {
25921
const isConfigured = isMCPConfigured()
26022
vscode.commands.executeCommand('setContext', 'codacy:mcpConfigured', isConfigured)
26123
}
26224

263-
export async function createOrUpdateRules(params?: RepositoryParams) {
264-
const analyzeGeneratedCode = vscode.workspace.getConfiguration().get('codacy.guardrails.analyzeGeneratedCode')
265-
const generateRules = vscode.workspace.getConfiguration().get('codacy.guardrails.rulesFile')
266-
267-
if (generateRules === 'disabled') return
268-
269-
const newRules = newRulesTemplate(params, analyzeGeneratedCode === 'disabled' ? ['guardrails'] : [])
270-
271-
try {
272-
const { path: rulesPath, format } = getCorrectRulesInfo()
273-
const isMdc = format === 'mdc'
274-
const dirPath = path.dirname(rulesPath)
275-
276-
// Create directories if they don't exist
277-
if (!fs.existsSync(dirPath)) {
278-
fs.mkdirSync(dirPath, { recursive: true })
279-
}
280-
281-
if (!fs.existsSync(rulesPath)) {
282-
fs.writeFileSync(rulesPath, `${isMdc ? rulesPrefixForMdc : ''}${convertRulesToMarkdown(newRules)}`)
283-
Logger.appendLine(`Created new rules file at ${rulesPath}`)
284-
addRulesToGitignore(rulesPath)
285-
} else {
286-
try {
287-
const existingContent = fs.readFileSync(rulesPath, 'utf8')
288-
289-
fs.writeFileSync(rulesPath, convertRulesToMarkdown(newRules, existingContent))
290-
291-
Logger.appendLine(`Updated rules in ${rulesPath}`)
292-
} catch (parseError) {
293-
Logger.error(`Error parsing existing rules file. Creating new one. Details: ${parseError}`)
294-
fs.writeFileSync(rulesPath, JSON.stringify(newRules, null, 2))
295-
}
296-
}
297-
} catch (error: unknown) {
298-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'
299-
Logger.error(`Failed to create rules: ${errorMessage}`)
300-
throw error
301-
}
302-
}
303-
30425
/**
30526
* Detects if the current environment is running in Windows Subsystem for Linux (WSL)
30627
* @returns boolean indicating if the current environment is WSL
@@ -319,7 +40,7 @@ export function isRunningInWsl(): boolean {
31940
}
32041
}
32142

322-
function getCurrentIDE(): string {
43+
export function getCurrentIDE(): string {
32344
const isCursor = vscode.env.appName.toLowerCase().includes('cursor')
32445
const isWindsurf = vscode.env.appName.toLowerCase().includes('windsurf')
32546
const isVSCodeInsiders = vscode.env.appName.toLowerCase().includes('insiders')

0 commit comments

Comments
 (0)