diff --git a/package.json b/package.json index 8e48859d..965cae72 100644 --- a/package.json +++ b/package.json @@ -473,6 +473,10 @@ "command": "Tools-for-Solidity.generate.imports_graph", "title": "Solidity: Generate Imports Graph" }, + { + "command": "Tools-for-Solidity.generate.abstraction_resolved", + "title": "Solidity: Generate Abstraction Resolved File" + }, { "command": "Tools-for-Solidity.generate.inheritance_graph", "title": "Solidity: Generate Inheritance Graph" @@ -1115,7 +1119,7 @@ "@google-cloud/storage": "^7.11.2", "@hpcc-js/wasm": "^2.1.0", "@renovatebot/pep440": "^2.0.0", - "@solidity-parser/parser": "^0.19.0", + "@solidity-parser/parser": "^0.20.1", "applicationinsights": "^2.9.0", "camelcase-keys": "^9.1.3", "crypto": "^1.0.1", diff --git a/src/Analytics.ts b/src/Analytics.ts index 4369a19c..3d01bcc5 100644 --- a/src/Analytics.ts +++ b/src/Analytics.ts @@ -7,17 +7,21 @@ let appInsights = require('applicationinsights'); class TelemetrySender implements vscode.TelemetrySender { sendEventData(eventName: string, data?: Record | undefined): void { - appInsights.defaultClient.trackEvent({ - name: eventName, - properties: data - }); + if (appInsights.defaultClient) { + appInsights.defaultClient.trackEvent({ + name: eventName, + properties: data + }); + } } sendErrorData(error: Error, data?: Record | undefined): void { - appInsights.defaultClient.trackException({ - exception: error, - properties: data - }); + if (appInsights.defaultClient) { + appInsights.defaultClient.trackException({ + exception: error, + properties: data + }); + } } } @@ -28,31 +32,44 @@ export class Analytics { correctPythonPath: boolean | undefined; correctSysPath: boolean | undefined; installation: string | undefined; + private isInitialized: boolean = false; public initialize(context: vscode.ExtensionContext, installation: string) { - appInsights - .setup(env.TELEMETRY_KEY) - .setAutoCollectRequests(false) - .setAutoCollectPerformance(false) - .setAutoCollectExceptions(false) - .setAutoCollectDependencies(false) - .setAutoDependencyCorrelation(false) - .setAutoCollectConsole(false) - .setUseDiskRetryCaching(true) - .start(); - const { userId, sessionId } = appInsights.defaultClient.context.keys; - appInsights.defaultClient.context.tags[userId] = vscode.env.machineId; - appInsights.defaultClient.context.tags[sessionId] = vscode.env.sessionId; - - this.context = context; - this.telemetryLogger = vscode.env.createTelemetryLogger(new TelemetrySender()); - this.wakeVersion = undefined; - this.correctPythonPath = undefined; - this.correctSysPath = undefined; - - this.installation = installation; - - context.subscriptions.push(this.telemetryLogger); + try { + // Only initialize Application Insights if we have a valid telemetry key + if (env.TELEMETRY_KEY && env.TELEMETRY_KEY.trim() !== '') { + appInsights + .setup(env.TELEMETRY_KEY) + .setAutoCollectRequests(false) + .setAutoCollectPerformance(false) + .setAutoCollectExceptions(false) + .setAutoCollectDependencies(false) + .setAutoDependencyCorrelation(false) + .setAutoCollectConsole(false) + .setUseDiskRetryCaching(true) + .start(); + + const { userId, sessionId } = appInsights.defaultClient.context.keys; + appInsights.defaultClient.context.tags[userId] = vscode.env.machineId; + appInsights.defaultClient.context.tags[sessionId] = vscode.env.sessionId; + } else { + console.log('Telemetry key not provided, Application Insights disabled'); + } + + this.context = context; + this.telemetryLogger = vscode.env.createTelemetryLogger(new TelemetrySender()); + this.wakeVersion = undefined; + this.correctPythonPath = undefined; + this.correctSysPath = undefined; + this.installation = installation; + this.isInitialized = true; + + context.subscriptions.push(this.telemetryLogger); + } catch (error) { + console.error('Failed to initialize Analytics:', error); + // Don't throw the error - allow extension to continue without telemetry + this.isInitialized = false; + } } public setWakeVersion(version: string) { @@ -76,57 +93,65 @@ export class Analytics { } logEvent(name: string) { - if (this.telemetryLogger === undefined || this.context === undefined) { - console.error('Cannot log event: TelemetryLogger or context is undefined'); + if (!this.isInitialized || this.telemetryLogger === undefined || this.context === undefined) { + console.log(`Analytics not initialized, skipping event: ${name}`); return; } - this.telemetryLogger.logUsage(name, { - 'common.extname': this.context.extension.packageJSON.name as string, - 'common.extversion': this.context.extension.packageJSON.version as string, - 'common.vscodemachineid': vscode.env.machineId, - 'common.vscodeseesionid': vscode.env.sessionId, - 'common.vscodeversion': vscode.version, - 'common.os': process.platform.toString(), - 'common.nodeArch': process.arch, - installation: this.installation, - 'wake.version': this.wakeVersion || 'unknown' - }); + try { + this.telemetryLogger.logUsage(name, { + 'common.extname': this.context.extension.packageJSON.name as string, + 'common.extversion': this.context.extension.packageJSON.version as string, + 'common.vscodemachineid': vscode.env.machineId, + 'common.vscodeseesionid': vscode.env.sessionId, + 'common.vscodeversion': vscode.version, + 'common.os': process.platform.toString(), + 'common.nodeArch': process.arch, + installation: this.installation, + 'wake.version': this.wakeVersion || 'unknown' + }); + } catch (error) { + console.error('Failed to log event:', error); + } } logCrash(event: EventType, error: any) { - if (this.telemetryLogger === undefined || this.context === undefined) { - console.error('Cannot log event: TelemetryLogger or context is undefined'); + if (!this.isInitialized || this.telemetryLogger === undefined || this.context === undefined) { + console.log(`Analytics not initialized, skipping crash log: ${event}`); return; } - let data: any = { - 'common.extname': this.context.extension.packageJSON.name as string, - 'common.extversion': this.context.extension.packageJSON.version as string, - 'common.vscodemachineid': vscode.env.machineId, - 'common.vscodeseesionid': vscode.env.sessionId, - 'common.vscodeversion': vscode.version, - 'common.os': process.platform.toString(), - 'common.nodeArch': process.arch, - installation: this.installation, - 'wake.version': this.wakeVersion || 'unknown', - error: error.toString().slice(-8100), - correctPythonPath: this.correctPythonPath, - correctSysPath: this.correctSysPath - }; - - if (event === EventType.ERROR_SAKE) { - const app = appState.get(); - data = { - ...data, - 'sake.appstate.initializationState': app.initializationState, - 'sake.appstate.isAnvilInstalled': app.isAnvilInstalled, - 'sake.appstate.isWakeServerRunning': app.isWakeServerRunning, - 'sake.appstate.isOpenWorkspace': app.isOpenWorkspace, - 'sake.chainstate.currentChainId': extensionState.get().currentChainId + try { + let data: any = { + 'common.extname': this.context.extension.packageJSON.name as string, + 'common.extversion': this.context.extension.packageJSON.version as string, + 'common.vscodemachineid': vscode.env.machineId, + 'common.vscodeseesionid': vscode.env.sessionId, + 'common.vscodeversion': vscode.version, + 'common.os': process.platform.toString(), + 'common.nodeArch': process.arch, + installation: this.installation, + 'wake.version': this.wakeVersion || 'unknown', + error: error.toString().slice(-8100), + correctPythonPath: this.correctPythonPath, + correctSysPath: this.correctSysPath }; + + if (event === EventType.ERROR_SAKE) { + const app = appState.get(); + data = { + ...data, + 'sake.appstate.initializationState': app.initializationState, + 'sake.appstate.isAnvilInstalled': app.isAnvilInstalled, + 'sake.appstate.isWakeServerRunning': app.isWakeServerRunning, + 'sake.appstate.isOpenWorkspace': app.isOpenWorkspace, + 'sake.chainstate.currentChainId': extensionState.get().currentChainId + }; + } + this.telemetryLogger.logError(event, data); + } catch (error) { + console.error('Failed to log crash:', error); } - this.telemetryLogger.logError(event, data); } } diff --git a/src/commands.ts b/src/commands.ts index 18c6f8f3..6a32afb8 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -4,8 +4,9 @@ import { URI, Position, LanguageClient, State } from 'vscode-languageclient/node import * as os from 'os'; import { GraphvizPreviewGenerator } from './graphviz/GraphvizPreviewGenerator'; import { SakeContext } from './sake/context'; - -const fs = require('fs'); +import parser from '@solidity-parser/parser'; +import * as fs from 'fs'; +import * as path from 'path'; async function showDot(content: string, title: string, graphviz: GraphvizPreviewGenerator) { const panel = graphviz.createPreviewPanel(content, title, vscode.ViewColumn.Beside); @@ -140,6 +141,247 @@ export async function generateImportsGraphHandler( await showDot(graph, 'Imports graph', graphviz); } +export async function generateAbstractionResolvedHandler( + out: vscode.OutputChannel +) { + try { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor || activeEditor.document.languageId !== 'solidity') { + vscode.window.showErrorMessage('Please open a Solidity file to generate abstraction resolved version.'); + return; + } + + const document = activeEditor.document; + const fileName = path.basename(document.fileName, '.sol'); + const fileDir = path.dirname(document.fileName); + const code = document.getText(); + + // Parse the AST + let ast; + try { + ast = parser.parse(code, { loc: true }); + } catch (e) { + const errMsg = e instanceof Error ? e.message : String(e); + vscode.window.showErrorMessage('Failed to parse Solidity file: ' + errMsg); + return; + } + + // Helper to resolve imports recursively + const contractMap: Record = {}; + const importMap: Record = {}; + function resolveImports(ast: any, baseDir: string) { + parser.visit(ast, { + ImportDirective: (node: any) => { + let importPath = node.path; + if (!importPath.endsWith('.sol')) return; + let resolvedPath = path.resolve(baseDir, importPath); + if (!fs.existsSync(resolvedPath)) return; + if (importMap[importPath]) return; // already loaded + importMap[importPath] = resolvedPath; + const importCode = fs.readFileSync(resolvedPath, 'utf8'); + let importAst; + try { + importAst = parser.parse(importCode, { loc: true }); + } catch (e) { return; } + resolveImports(importAst, path.dirname(resolvedPath)); + parser.visit(importAst, { + ContractDefinition: (c: any) => { + contractMap[c.name] = c; + } + }); + }, + ContractDefinition: (c: any) => { + contractMap[c.name] = c; + } + }); + } + + // Collect all contracts in the current file and imports + resolveImports(ast, fileDir); + + // Find the main contract (the one at the cursor or first in file) + let mainContract: any = null; + parser.visit(ast, { + ContractDefinition: (c: any) => { + if (!mainContract) mainContract = c; + } + }); + if (!mainContract) { + vscode.window.showErrorMessage('No contract found in file.'); + return; + } + + // Recursively collect all base contracts (linearized) + function getBaseContracts(contract: any): string[] { + let bases: string[] = []; + if (contract.baseContracts) { + for (const base of contract.baseContracts) { + const baseName = base.baseName.namePath; + if (contractMap[baseName]) { + bases.push(baseName); + bases = bases.concat(getBaseContracts(contractMap[baseName])); + } + } + } + return bases; + } + const baseContracts = getBaseContracts(mainContract); + const allContracts = [...baseContracts.reverse(), mainContract.name]; + + // Helper to extract source code by AST location + function extractSourceByLoc(loc: any) { + if (!loc) return ''; + const start = document.offsetAt(new vscode.Position(loc.start.line - 1, loc.start.column)); + const end = document.offsetAt(new vscode.Position(loc.end.line - 1, loc.end.column)); + return document.getText().slice(start, end); + } + + // Helper to extract comments above a node + function extractCommentsAbove(loc: any) { + if (!loc) return ''; + const startLine = loc.start.line - 2; // line before the node + if (startLine < 0) return ''; + const lines = document.getText().split(/\r?\n/); + let comments = ''; + for (let i = startLine; i >= 0; i--) { + const line = lines[i].trim(); + if (line.startsWith('//') || line.startsWith('/*')) { + comments = line + '\n' + comments; + } else if (line === '') { + continue; + } else { + break; + } + } + return comments; + } + + // Helper to collect all contract members (with override handling) + function collectMembers(contracts: string[]) { + const seenFuncs = new Set(); + const seenVars = new Set(); + const seenEvents = new Set(); + const seenMods = new Set(); + const variables: any[] = []; + const events: any[] = []; + const modifiers: any[] = []; + const functions: any[] = []; + for (const cname of contracts) { + const c = contractMap[cname]; + if (!c) continue; + for (const node of c.subNodes) { + if (node.type === 'FunctionDefinition' && (node.name || node.isConstructor)) { + const key = node.isConstructor ? 'constructor' : node.name; + if (seenFuncs.has(key)) continue; + seenFuncs.add(key); + functions.push({ ...node, _contract: cname, _type: 'function' }); + } + if (node.type === 'StateVariableDeclaration') { + for (const varDecl of node.variables) { + if (seenVars.has(varDecl.name)) continue; + seenVars.add(varDecl.name); + variables.push({ ...varDecl, _contract: cname, _type: 'variable', visibility: node.visibility, typeName: varDecl.typeName || node.typeName, loc: node.loc }); + } + } + if (node.type === 'EventDefinition') { + if (seenEvents.has(node.name)) continue; + seenEvents.add(node.name); + events.push({ ...node, _contract: cname, _type: 'event' }); + } + if (node.type === 'ModifierDefinition') { + if (seenMods.has(node.name)) continue; + seenMods.add(node.name); + modifiers.push({ ...node, _contract: cname, _type: 'modifier' }); + } + } + } + return { variables, events, modifiers, functions }; + } + + // Generate the flattened contract + let flattened = `// Abstraction Resolved File for ${mainContract.name}\n`; + flattened += `// Generated on: ${new Date().toISOString()}\n`; + flattened += `${document.getText().match(/pragma solidity [^;]+;/)?.[0] || ''}\n\n`; + flattened += `contract ${mainContract.name} {\n`; + const { variables, events, modifiers, functions } = collectMembers([...baseContracts.reverse(), mainContract.name]); + // Variables + if (variables.length > 0) flattened += ' // ---- State Variables ----\n'; + for (const v of variables) { + flattened += extractCommentsAbove(v.loc); + flattened += ` // from ${v._contract}\n`; + flattened += ` ${v.visibility || ''} ${v.typeName?.name || v.typeName?.typeDescriptions?.typeString || ''} ${v.name};\n\n`; + } + // Events + if (events.length > 0) flattened += ' // ---- Events ----\n'; + for (const e of events) { + flattened += extractCommentsAbove(e.loc); + flattened += ` // from ${e._contract}\n`; + flattened += ` event ${e.name}(${(e.parameters || []).map((p: any) => `${p.typeName?.name || p.typeName?.typeDescriptions?.typeString || ''} ${p.name}`).join(', ')});\n\n`; + } + // Modifiers + if (modifiers.length > 0) flattened += ' // ---- Modifiers ----\n'; + for (const m of modifiers) { + flattened += extractCommentsAbove(m.loc); + flattened += ` // from ${m._contract}\n`; + let body = ' { /* ... */ }'; + if (m.body && m.loc && m.body.loc) { + body = extractSourceByLoc(m.body.loc); + if (!body.trim().endsWith('}')) body += '}'; + } + flattened += ` modifier ${m.name}()${body}\n\n`; + } + // Functions + if (functions.length > 0) flattened += ' // ---- Functions ----\n'; + for (const f of functions) { + flattened += extractCommentsAbove(f.loc); + flattened += ` // from ${f._contract}\n`; + // Build function signature + let sig = ''; + if (f.isConstructor) { + sig = 'constructor('; + if (f.parameters && f.parameters.length > 0) { + sig += f.parameters.map((p: any) => `${p.typeName?.name || p.typeName?.typeDescriptions?.typeString || ''} ${p.name}`).join(', '); + } + sig += ')'; + } else { + sig = `function ${f.name}(`; + if (f.parameters && f.parameters.length > 0) { + sig += f.parameters.map((p: any) => `${p.typeName?.name || p.typeName?.typeDescriptions?.typeString || ''} ${p.name}`).join(', '); + } + sig += ')'; + if (f.visibility) sig += ` ${f.visibility}`; + if (f.stateMutability && f.stateMutability !== 'nonpayable') sig += ` ${f.stateMutability}`; + if (f.returnParameters && f.returnParameters.length > 0) { + sig += ' returns (' + f.returnParameters.map((p: any) => `${p.typeName?.name || p.typeName?.typeDescriptions?.typeString || ''} ${p.name}`).join(', ') + ')'; + } + } + // Function body + let body = ' { /* ... */ }'; + if (f.body && f.loc && f.body.loc) { + body = extractSourceByLoc(f.body.loc); + if (!body.trim().endsWith('}')) body += '}'; + } else if (f._contract !== mainContract.name) { + // Inherited function, no source: stub + body = ' { /* inherited */ }'; + } + flattened += ` ${sig}${body}\n\n`; + } + flattened += '}\n'; + + // Create a new document with the resolved content + const newDocument = await vscode.workspace.openTextDocument({ + content: flattened, + language: 'solidity' + }); + await vscode.window.showTextDocument(newDocument, vscode.ViewColumn.Beside); + out.appendLine(`✅ Abstraction resolved file generated for ${mainContract.name}`); + vscode.window.showInformationMessage(`Abstraction resolved file generated for ${mainContract.name}`); + } catch (error) { + out.appendLine(`❌ Error generating abstraction resolved file: ${error}`); + vscode.window.showErrorMessage(`Error generating abstraction resolved file: ${error}`); + } +} + export async function executeReferencesHandler( out: vscode.OutputChannel, documentUri: URI, diff --git a/src/detections/util.ts b/src/detections/util.ts index 4c3f3a04..72e6ab32 100644 --- a/src/detections/util.ts +++ b/src/detections/util.ts @@ -44,4 +44,79 @@ export function convertDiagnostics(it: Diagnostic): WakeDiagnostic { function convertRange(it : Range) : vscode.Range{ return new vscode.Range(new vscode.Position(it.start.line, it.start.character), new vscode.Position(it.end.line, it.end.character)) -} \ No newline at end of file +} + +export interface IgnoreComment { + line: number; + detectorId?: string; + isGlobal: boolean; +} + +export function parseIgnoreComments(document: vscode.TextDocument): IgnoreComment[] { + const ignoreComments: IgnoreComment[] = []; + const ignorePattern = /\/\/\s*(wake-ignore|wake-disable|wake-suppress)(?:\s+(\w+))?/i; + const blockIgnorePattern = /\/\*\s*(wake-ignore|wake-disable|wake-suppress)(?:\s+(\w+))?\s*\*\//i; + + for (let i = 0; i < document.lineCount; i++) { + const line = document.lineAt(i); + const lineText = line.text; + + // Check for line comments + const lineMatch = lineText.match(ignorePattern); + if (lineMatch) { + ignoreComments.push({ + line: i, + detectorId: lineMatch[2] || undefined, + isGlobal: !lineMatch[2] + }); + continue; + } + + // Check for block comments on the same line + const blockMatch = lineText.match(blockIgnorePattern); + if (blockMatch) { + ignoreComments.push({ + line: i, + detectorId: blockMatch[2] || undefined, + isGlobal: !blockMatch[2] + }); + } + } + + return ignoreComments; +} + +export function shouldIgnoreDetection( + detection: WakeDiagnostic, + ignoreComments: IgnoreComment[] +): boolean { + const detectionLine = detection.range.start.line; + + for (const ignoreComment of ignoreComments) { + // Check if the ignore comment is on the same line or the line before + if (ignoreComment.line === detectionLine || ignoreComment.line === detectionLine - 1) { + // If it's a global ignore (no specific detector), ignore all detections + if (ignoreComment.isGlobal) { + return true; + } + + // If it's a specific detector ignore, check if the detector ID matches + if (ignoreComment.detectorId && detection.code) { + let detectorId: string; + if (typeof detection.code === 'string') { + detectorId = detection.code; + } else if (detection.code && typeof detection.code === 'object' && 'value' in detection.code) { + detectorId = String(detection.code.value); + } else { + continue; + } + + if (detectorId === ignoreComment.detectorId) { + return true; + } + } + } + } + + return false; +} diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000..3cf52bc6 --- /dev/null +++ b/src/env.ts @@ -0,0 +1 @@ +export const TELEMETRY_KEY: string = "" \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index f2dffd14..506d794c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,6 +27,7 @@ import { generateInheritanceGraphHandler, generateLinearizedInheritanceGraphHandler, generateImportsGraphHandler, + generateAbstractionResolvedHandler, executeReferencesHandler, newDetector, newPrinter @@ -39,7 +40,7 @@ import { GroupBy, Impact, Confidence } from './detections/WakeTreeDataProvider'; import { SolcTreeDataProvider } from './detections/SolcTreeDataProvider'; import { WakeTreeDataProvider } from './detections/WakeTreeDataProvider'; import { Detector, WakeDetection } from './detections/model/WakeDetection'; -import { convertDiagnostics } from './detections/util'; +import { convertDiagnostics, parseIgnoreComments, shouldIgnoreDetection } from './detections/util'; import { DetectorItem } from './detections/model/DetectorItem'; import { ClientMiddleware } from './ClientMiddleware'; import { ClientErrorHandler } from './ClientErrorHandler'; @@ -75,14 +76,27 @@ interface DiagnosticNotification { diagnostics: Diagnostic[]; } -function onNotification(outputChannel: vscode.OutputChannel, detection: DiagnosticNotification) { +async function onNotification(outputChannel: vscode.OutputChannel, detection: DiagnosticNotification) { let diags = detection.diagnostics .map((it) => convertDiagnostics(it)) .filter((item) => !item.data.ignored); - diagnosticCollection.set(vscode.Uri.parse(detection.uri), diags); + + // Parse ignore comments from the document + const uri = vscode.Uri.parse(detection.uri); + let ignoreComments: any[] = []; + try { + const document = await vscode.workspace.openTextDocument(uri); + ignoreComments = parseIgnoreComments(document); + } catch (err) { + // Document might not be open, ignore comments will be empty + } + + // Filter out detections that should be ignored based on comments + diags = diags.filter((item) => !shouldIgnoreDetection(item, ignoreComments)); + + diagnosticCollection.set(uri, diags); try { - let uri = vscode.Uri.parse(detection.uri); let wakeDetections = diags .filter((item) => item.source == 'Wake') .map((it) => new WakeDetection(uri, it)); @@ -261,7 +275,7 @@ export async function activate(context: vscode.ExtensionContext) { diagnosticCollection = vscode.languages.createDiagnosticCollection('Wake'); - client.onNotification('textDocument/publishDiagnostics', (params) => { + client.onNotification('textDocument/publishDiagnostics', async (params) => { //outputChannel.appendLine(JSON.stringify(params)); let diag = params as DiagnosticNotification; @@ -276,7 +290,7 @@ export async function activate(context: vscode.ExtensionContext) { } return true; }); - onNotification(outputChannel, diag); + await onNotification(outputChannel, diag); }); client.onNotification(ShowMessageNotification.type, (message) => { switch (message.type) { @@ -408,6 +422,12 @@ function registerCommands(outputChannel: vscode.OutputChannel, context: vscode.E async () => await generateImportsGraphHandler(outputChannel, graphvizGenerator) ) ); + context.subscriptions.push( + vscode.commands.registerCommand( + 'Tools-for-Solidity.generate.abstraction_resolved', + async () => await generateAbstractionResolvedHandler(outputChannel) + ) + ); context.subscriptions.push( vscode.commands.registerCommand( 'Tools-for-Solidity.execute.references', diff --git a/syntaxes/solidity.tmLanguage.json b/syntaxes/solidity.tmLanguage.json index 95095119..3a971888 100644 --- a/syntaxes/solidity.tmLanguage.json +++ b/syntaxes/solidity.tmLanguage.json @@ -153,6 +153,10 @@ "match": "(?i)\\b(FIXME|TODO|CHANGED|XXX|AUDIT|IDEA|HACK|NOTE|REVIEW|NB|BUG|QUESTION|COMBAK|TEMP|SUPPRESS|LINT|\\w+-disable|\\w+-suppress)\\b(?-i)", "name": "keyword.comment.todo" }, + "comment-wake-ignore": { + "match": "(?i)\\b(wake-ignore|wake-disable|wake-suppress)\\b(?-i)", + "name": "keyword.comment.wake-ignore" + }, "comment-line": { "begin": "(?