diff --git a/document/_java.notCoveredExecution.md b/document/_java.notCoveredExecution.md new file mode 100644 index 0000000000..a2859646cc --- /dev/null +++ b/document/_java.notCoveredExecution.md @@ -0,0 +1,109 @@ +# Not Covered Maven Plugin Execution + +## Background +Some Maven projects use 3rd party Maven plugins to generate sources or resources, and you may find that the generated code is not picked up by the project classpath, or the Maven goals are not executed by Java extension. This is a known technical issue with `m2e`, the underlying Maven integration tool for Java extension. The reason is that the Maven tooling `m2e` doesn't know if it's safe to run your Maven plugin automatically during a workspace build, so it does not run them by default and requires explicit instructions on how to handle them. Learn more about this issue from the wiki about [Execution Not Covered](https://www.eclipse.org/m2e/documentation/m2e-execution-not-covered.html). + +## Workaround +- Option 1: Use [build-helper-maven-plugin](http://www.mojohaus.org/build-helper-maven-plugin/usage.html) to explicitly add the unrecoginzed source folder to classpath. + +```xml + + ... + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + add-source + generate-sources + + add-source + + + + some directory + ... + + + + + + + + +``` + +- Option 2: Configure a lifecycle mapping metadata in pom.xml that explicitly tells m2e what to do with your plugin. + +For example, add [a processing instruction in the pom.xml](https://www.eclipse.org/m2e/documentation/release-notes-17.html#new-syntax-for-specifying-lifecycle-mapping-metadata) like `` to execute it on every project configuration udpate. + +You can use quick fixes to generate the inline lifecycle mapping in pom.xml, or manually configure it in pom.xml. If it's yourself that manually configure it, you have to let VS Code update the project configuration as well. The command is `"Java: Update Project"`. + +```xml + + ... + + + + ro.isdc.wro4j + wro4j-maven-plugin + 1.8.0 + + + + generate-resources + + run + + + + + + + +``` + +If you have multiple Maven plugins that need to configure the lifecycle mapping metadata, you can also configure them together in a dedicated `pluginManagement` section. + +```xml + + ... + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + ro.isdc.wro4j + wro4j-maven-plugin + [1.8.0,) + + run + + + + + true + + + + + + + + + + + +``` diff --git a/package.json b/package.json index 8f6c0659ca..84afbac2d9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "workspaceContains:pom.xml", "workspaceContains:build.gradle", "workspaceContains:.classpath", - "onCommand:_java.templateVariables" + "onCommand:_java.templateVariables", + "onCommand:_java.notCoveredExecution" ], "main": "./dist/extension", "contributes": { @@ -309,6 +310,17 @@ "description": "Path to Maven's global settings.xml", "scope": "window" }, + "java.configuration.maven.notCoveredPluginExecutionSeverity": { + "type": "string", + "enum": [ + "ignore", + "warning", + "error" + ], + "default": "error", + "description": "Specifies severity if the plugin execution is not covered by Maven build lifecycle.", + "scope": "window" + }, "java.format.enabled": { "type": "boolean", "default": true, diff --git a/src/commands.ts b/src/commands.ts index 9448b87f0a..8fe8e8988c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -247,5 +247,7 @@ export namespace Commands { export const TEMPLATE_VARIABLES = '_java.templateVariables'; + export const NOT_COVERED_EXECUTION = '_java.notCoveredExecution'; + export const RUNTIME_VALIDATION_OPEN = 'java.runtimeValidation.open'; } diff --git a/src/extension.ts b/src/extension.ts index 94e79fbc7b..00e11a7b5a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -119,6 +119,9 @@ export function activate(context: ExtensionContext): Promise { context.subscriptions.push(commands.registerCommand(Commands.TEMPLATE_VARIABLES, async () => { markdownPreviewProvider.show(context.asAbsolutePath(path.join('document', `${Commands.TEMPLATE_VARIABLES}.md`)), 'Predefined Variables', "", context); })); + context.subscriptions.push(commands.registerCommand(Commands.NOT_COVERED_EXECUTION, async () => { + markdownPreviewProvider.show(context.asAbsolutePath(path.join('document', `_java.notCoveredExecution.md`)), 'Not Covered Maven Plugin Execution', "", context); + })); let storagePath = context.storagePath; if (!storagePath) { diff --git a/src/pom/pomCodeActionProvider.ts b/src/pom/pomCodeActionProvider.ts new file mode 100644 index 0000000000..1ce2da2d1b --- /dev/null +++ b/src/pom/pomCodeActionProvider.ts @@ -0,0 +1,92 @@ +'use strict'; + +import { CodeActionProvider, CodeAction, TextDocument, Range, Selection, CodeActionContext, CancellationToken, ProviderResult, Command, CodeActionKind, Diagnostic, WorkspaceEdit, Position, EndOfLine, ExtensionContext, commands, CodeActionProviderMetadata, workspace, Uri } from "vscode"; +import { Commands } from "../commands"; + +export class PomCodeActionProvider implements CodeActionProvider { + constructor(context: ExtensionContext) { + context.subscriptions.push(commands.registerCommand("_java.projectConfiguration.saveAndUpdate", async (uri: Uri) => { + const document: TextDocument = await workspace.openTextDocument(uri); + await document.save(); + commands.executeCommand(Commands.CONFIGURATION_UPDATE, uri); + })); + } + + provideCodeActions(document: TextDocument, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | CodeAction)[]> { + if (context?.diagnostics?.length && context.diagnostics[0].source === "Java") { + return this.collectCodeActionsForNotCoveredExecutions(document, context.diagnostics); + } + + return undefined; + } + + private collectCodeActionsForNotCoveredExecutions(document: TextDocument, diagnostics: readonly Diagnostic[]): CodeAction[] { + const codeActions: CodeAction[] = []; + for (const diagnostic of diagnostics) { + if (diagnostic.message?.startsWith("Plugin execution not covered by lifecycle configuration")) { + const indentation = this.getNewTextIndentation(document, diagnostic); + const saveAndUpdateConfigCommand: Command = { + title: "Save and update project configuration", + command: "_java.projectConfiguration.saveAndUpdate", + arguments: [ document.uri ], + }; + + const action1 = new CodeAction("Enable this execution in project configuration phase", CodeActionKind.QuickFix.append("pom")); + action1.edit = new WorkspaceEdit(); + action1.edit.insert(document.uri, new Position(diagnostic.range.end.line, diagnostic.range.end.character + 1), + indentation + ""); + action1.command = saveAndUpdateConfigCommand; + codeActions.push(action1); + + const action2 = new CodeAction("Enable this execution in project build phase", CodeActionKind.QuickFix.append("pom")); + action2.edit = new WorkspaceEdit(); + action2.edit.insert(document.uri, new Position(diagnostic.range.end.line, diagnostic.range.end.character + 1), + indentation + ""); + action2.command = saveAndUpdateConfigCommand; + codeActions.push(action2); + + const action3 = new CodeAction("Mark this execution as ignored in pom.xml", CodeActionKind.QuickFix.append("pom")); + action3.edit = new WorkspaceEdit(); + action3.edit.insert(document.uri, new Position(diagnostic.range.end.line, diagnostic.range.end.character + 1), + indentation + ""); + action3.command = saveAndUpdateConfigCommand; + codeActions.push(action3); + } + } + + return codeActions; + } + + private getNewTextIndentation(document: TextDocument, diagnostic: Diagnostic): string { + const textline = document.lineAt(diagnostic.range.end.line); + if (textline.text.lastIndexOf("") > diagnostic.range.end.character) { + return ""; + } + + const lineSeparator = document.eol === EndOfLine.LF ? "\r" : "\r\n"; + const currentTextIndentation = textline.text.substring(0, textline.firstNonWhitespaceCharacterIndex); + const tabSize = currentTextIndentation.length / 5; + const insertChar = currentTextIndentation && currentTextIndentation.charAt(0) === ' ' ? ' ' : ' '; + let newIndentation = lineSeparator + currentTextIndentation; + for (let i = 0; i < tabSize; i++) { + newIndentation += insertChar; + } + + return newIndentation; + } +} + +export const pomCodeActionMetadata: CodeActionProviderMetadata = { + providedCodeActionKinds: [ + CodeActionKind.QuickFix.append("pom") + ], + documentation: [ + { + kind: CodeActionKind.QuickFix.append("pom"), + command: { + title: "Learn more about not covered Maven execution", + command: Commands.NOT_COVERED_EXECUTION + } + } + ], +}; diff --git a/src/standardLanguageClient.ts b/src/standardLanguageClient.ts index 81003303ed..60dc9efdb4 100644 --- a/src/standardLanguageClient.ts +++ b/src/standardLanguageClient.ts @@ -30,6 +30,7 @@ import { RefactorDocumentProvider, javaRefactorKinds } from "./codeActionProvide import { typeHierarchyTree } from "./typeHierarchy/typeHierarchyTree"; import { TypeHierarchyDirection, TypeHierarchyItem } from "./typeHierarchy/protocol"; import { buildFilePatterns } from './plugin'; +import { pomCodeActionMetadata, PomCodeActionProvider } from "./pom/pomCodeActionProvider"; const extensionName = 'Language Support for Java'; const GRADLE_CHECKSUM = "gradle/checksum/prompt"; @@ -410,6 +411,12 @@ export class StandardLanguageClient { const sectionId: string = javaRefactorKinds.get(kind) || ''; markdownPreviewProvider.show(context.asAbsolutePath(path.join('document', `${Commands.LEARN_MORE_ABOUT_REFACTORING}.md`)), 'Java Refactoring', sectionId, context); })); + + languages.registerCodeActionsProvider({ + language: "xml", + scheme: "file", + pattern: "**/pom.xml" + }, new PomCodeActionProvider(context), pomCodeActionMetadata); }); }