From 413b1613852dcf741e4c4f10c7ad35e9c386fb9c Mon Sep 17 00:00:00 2001 From: Marat-Tim Date: Wed, 23 Apr 2025 16:59:04 +0300 Subject: [PATCH] feat(#15): markdownlint for check comments --- pom.xml | 63 +++++++++++ src/main/java/org/eolang/lints/Defect.java | 15 +++ .../lints/LtInvalidMarkdownComment.java | 92 ++++++++++++++++ .../java/org/eolang/lints/MarkdownLinter.java | 103 +++++++++++++++++ src/main/java/org/eolang/lints/WpaLints.java | 3 +- src/main/javascript/markdownlint.js | 20 ++++ src/main/javascript/package.json | 15 +++ src/main/javascript/webpack.config.js | 24 ++++ .../comments/invalid-markdown-comment.md | 104 ++++++++++++++++++ .../lints/LtInvalidMarkdownCommentTest.java | 83 ++++++++++++++ 10 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/eolang/lints/LtInvalidMarkdownComment.java create mode 100644 src/main/java/org/eolang/lints/MarkdownLinter.java create mode 100644 src/main/javascript/markdownlint.js create mode 100644 src/main/javascript/package.json create mode 100644 src/main/javascript/webpack.config.js create mode 100644 src/main/resources/org/eolang/motives/comments/invalid-markdown-comment.md create mode 100644 src/test/java/org/eolang/lints/LtInvalidMarkdownCommentTest.java diff --git a/pom.xml b/pom.xml index 3d7495438..e4c4a6870 100644 --- a/pom.xml +++ b/pom.xml @@ -145,6 +145,17 @@ jsr305 3.0.2 + + org.graalvm.js + js + 22.2.0 + runtime + + + org.graalvm.js + js-scriptengine + 22.2.0 + com.yegor256 tojos @@ -716,6 +727,29 @@ + + org.apache.maven.plugins + maven-resources-plugin + + + + copy-javascript + process-resources + + copy-resources + + + ${project.build.directory}/javascript + + + src/main/javascript + false + + + + + + org.apache.maven.plugins maven-dependency-plugin @@ -740,6 +774,35 @@ + + com.github.eirslett + frontend-maven-plugin + 1.15.1 + + + install-node-and-npm + + install-node-and-npm + + + v18.17.1 + 9.8.1 + + + + npm-install + + npm + + process-resources + + install + ${project.build.directory}/javascript + . + + + + diff --git a/src/main/java/org/eolang/lints/Defect.java b/src/main/java/org/eolang/lints/Defect.java index 554bf7bdb..8f70e2f92 100644 --- a/src/main/java/org/eolang/lints/Defect.java +++ b/src/main/java/org/eolang/lints/Defect.java @@ -158,6 +158,21 @@ public Default( this(rule, severity, object, line, text, false); } + /** + * Ctor. + * @param rule Rule name + * @param severity Severity level + * @param line Line number + * @param text Description of the defect + * @checkstyle ParameterNumberCheck (5 lines) + */ + Default( + final String rule, final Severity severity, + final int line, final String text + ) { + this(rule, severity, "", line, text); + } + /** * Ctor. *

diff --git a/src/main/java/org/eolang/lints/LtInvalidMarkdownComment.java b/src/main/java/org/eolang/lints/LtInvalidMarkdownComment.java new file mode 100644 index 000000000..1f207af69 --- /dev/null +++ b/src/main/java/org/eolang/lints/LtInvalidMarkdownComment.java @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import com.github.lombrozo.xnav.Xnav; +import com.jcabi.xml.XML; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.cactoos.io.ResourceOf; +import org.cactoos.text.IoCheckedText; +import org.cactoos.text.TextOf; +import org.eolang.parser.OnDefault; + +/** + * Comments should not be violated from the point of view of markdownlint. + * This lint works for multiple files, as creating a MarkdownLinter takes a very long time + * and creating it many times is a bad idea. + * + * @since 0.0.47 + */ +final class LtInvalidMarkdownComment implements Lint> { + /** + * Markdownlint rule names to ignore. + */ + private static final Set IGNORED = Set.of( + "MD041", + "MD047", + "MD026" + ); + + @Override + public String name() { + return "invalid-markdown-comment"; + } + + @Override + public Collection defects(final Map pkg) throws IOException { + final Collection defects = new ArrayList<>(0); + try (MarkdownLinter mdlinter = new MarkdownLinter()) { + for (final XML xmir : pkg.values()) { + this.defectsOfXmir(xmir, mdlinter).forEach(defects::add); + } + } + return defects; + } + + @Override + public String motive() throws IOException { + return new IoCheckedText( + new TextOf( + new ResourceOf( + String.format( + "org/eolang/motives/comments/%s.md", + this.name() + ) + ) + ) + ).asString(); + } + + private Stream defectsOfXmir(final XML xmir, final MarkdownLinter mdlinter) { + final Stream.Builder defects = Stream.builder(); + final List comments = new Xnav(xmir.inner()).path("/object/comments/comment") + .collect(Collectors.toList()); + for (final Xnav comment : comments) { + final String text = comment.text().get().replace("\\n", "\n"); + final int length = text.split("\n", -1).length; + final int line = Integer.parseInt(comment.attribute("line").text().orElse("0")); + mdlinter.defects(text) + .filter(defect -> !LtInvalidMarkdownComment.IGNORED.contains(defect.rule())) + .map( + defect -> new Defect.Default( + this.name(), + defect.severity(), + new OnDefault(xmir).get(), + line - 1 - length + defect.line(), + defect.text() + ) + ) + .forEach(defects::add); + } + return defects.build(); + } +} diff --git a/src/main/java/org/eolang/lints/MarkdownLinter.java b/src/main/java/org/eolang/lints/MarkdownLinter.java new file mode 100644 index 000000000..0ecd81900 --- /dev/null +++ b/src/main/java/org/eolang/lints/MarkdownLinter.java @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import java.io.Closeable; +import java.util.stream.Stream; +import org.cactoos.io.ResourceOf; +import org.cactoos.io.UncheckedInput; +import org.cactoos.text.FormattedText; +import org.cactoos.text.TextOf; +import org.cactoos.text.UncheckedText; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Value; + +/** + * Markdownlint interop. It can take a very long time to create. + * + * @since 0.0.47 + */ +final class MarkdownLinter implements Closeable { + /** + * JavaScript code of markdownlint with function, that run markdownlint on single string. + */ + private static final String JSCODE = String.format( + "%s%s", + "globalThis.URL=class{constructor(input){this.href=input;}};", + new UncheckedText( + new TextOf( + new UncheckedInput( + new ResourceOf("markdownlint.js") + ) + ) + ).asString() + ); + + /** + * JavaScripts context with markdownlint. + */ + private final Context context; + + /** + * Function lint from JavaScript, that accept string and return rules violations. + */ + private final Value mdlint; + + @SuppressWarnings("PMD.ConstructorOnlyInitializesOrCallOtherConstructors") + MarkdownLinter() { + this.context = Context.newBuilder("js") + .option("engine.WarnInterpreterOnly", "false") + .build(); + this.context.eval("js", MarkdownLinter.JSCODE); + this.mdlint = this.context.getBindings("js").getMember("lint"); + } + + public Stream defects(final String text) { + final Stream.Builder defects = Stream.builder(); + final Value errors = this.mdlint.execute(text); + for (int idx = 0; idx < errors.getArraySize(); idx += 1) { + final Value error = errors.getArrayElement(idx); + final String rule = rule(error.getMember("ruleNames")); + defects.add( + new Defect.Default( + rule, + Severity.WARNING, + error.getMember("lineNumber").asInt(), + new UncheckedText( + new FormattedText( + "[%s] %s. See %s", + rule, + error.getMember("ruleDescription").asString(), + error.getMember("ruleInformation").asString() + ) + ).asString() + ) + ); + } + return defects.build(); + } + + @Override + public void close() { + this.context.close(); + } + + /** + * MD rule. + * + * @param names Javascript array of names. + * @return The first name starting with MD. + * @throws IllegalStateException If no name starting with MD is found. + */ + private static String rule(final Value names) { + for (int idx = 0; idx < names.getArraySize(); idx += 1) { + final String name = names.getArrayElement(idx).asString(); + if (name.startsWith("MD")) { + return name; + } + } + throw new IllegalStateException("cannot find name of markdown lint starting with MD"); + } +} diff --git a/src/main/java/org/eolang/lints/WpaLints.java b/src/main/java/org/eolang/lints/WpaLints.java index a5c34f51c..6411df862 100644 --- a/src/main/java/org/eolang/lints/WpaLints.java +++ b/src/main/java/org/eolang/lints/WpaLints.java @@ -26,7 +26,8 @@ final class WpaLints extends IterableEnvelope>> { new LtObjectIsNotUnique(), new LtAtomIsNotUnique(), new LtInconsistentArgs(), - new LtIncorrectNumberOfAttrs() + new LtIncorrectNumberOfAttrs(), + new LtInvalidMarkdownComment() ) ); } diff --git a/src/main/javascript/markdownlint.js b/src/main/javascript/markdownlint.js new file mode 100644 index 000000000..c0543440b --- /dev/null +++ b/src/main/javascript/markdownlint.js @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +/** + * @todo #15:45min Add eslint to repository https://github.com/eslint/eslint. + * We should integrate eslint into repository to monitor the quality of code written in java script. + */ +import { lint as lintSync } from "markdownlint/sync"; + +globalThis.lint = function(text) { + const options = { + "strings": { + "file": text + } + }; + + const results = lintSync(options); + return results["file"]; +} diff --git a/src/main/javascript/package.json b/src/main/javascript/package.json new file mode 100644 index 000000000..738a29578 --- /dev/null +++ b/src/main/javascript/package.json @@ -0,0 +1,15 @@ +{ + "type": "module", + "name": "markdownlint", + "version": "1.0.0", + "main": "markdownlint.js", + "scripts": { + "postinstall": "npx webpack" + }, + "dependencies": { + "markdownlint": "^0.37.4" + }, + "devDependencies": { + "webpack-cli": "^6.0.1" + } +} diff --git a/src/main/javascript/webpack.config.js b/src/main/javascript/webpack.config.js new file mode 100644 index 000000000..970d07b2d --- /dev/null +++ b/src/main/javascript/webpack.config.js @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +import path from 'path'; +import { fileURLToPath } from 'url'; + +export default { + entry: './markdownlint.js', + output: { + filename: '../classes/markdownlint.js', + path: path.dirname(fileURLToPath(import.meta.url)), + }, + mode: 'production', + target: ['webworker'], + resolve: { + fallback: { + fs: false, + path: false, + os: false, + assert: false, + }, + } +}; diff --git a/src/main/resources/org/eolang/motives/comments/invalid-markdown-comment.md b/src/main/resources/org/eolang/motives/comments/invalid-markdown-comment.md new file mode 100644 index 000000000..17e935c3c --- /dev/null +++ b/src/main/resources/org/eolang/motives/comments/invalid-markdown-comment.md @@ -0,0 +1,104 @@ +# Comments must be valid markdownlint texts + +Comments in the code must be checked for correctness from the point of view of +[markdownlint](https://github.com/DavidAnson/markdownlint) + +## Examples + +### [MD001](https://github.com/DavidAnson/markdownlint/blob/main/doc/md001.md) + +Incorrect: + +```eo +# # Main object. +# +# This is main object of program. +# +# ### Subsection of main object. +# +# Sub information about main object. +[] > foo +``` + +Output: + +```shell +[foo invalid-markdown-comment WARNING]:5 [MD001] Heading levels should only increment by one level at a time. See https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md001.md +``` + +Correct: + +```eo +# # Main object. +# +# This is main object of program. +# +# ## Subsection of main object. +# +# Sub information about main object. +[] > foo +``` + +### [MD004](https://github.com/DavidAnson/markdownlint/blob/main/doc/md004.md) + +Incorrect: + +```eo +# Main object. +# +# - Item1 +# + Item2 +# * Item3 +[] > foo +``` + +Output: + +```shell +[foo invalid-markdown-comment WARNING]:4 [MD004] Unordered list style. See https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md004.md +[foo invalid-markdown-comment WARNING]:5 [MD004] Unordered list style. See https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md004.md +``` + +Correct: + +```eo +# Main object. +# +# - Item1 +# - Item2 +# - Item3 +[] > foo +``` + +### [MD018](https://github.com/DavidAnson/markdownlint/blob/main/doc/md018.md) + +Incorrect: + +```eo +# #Main object. +[] > foo +``` + +Output: + +```shell +[foo invalid-markdown-comment WARNING]:1 [MD018] No space after hash on atx style heading. See https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md018.md +``` + +Correct: + +```eo +# # Main object. +[] > foo +``` + +## Other information + +You can find all the rules here + + +We ignore the following errors: + +* [MD041](https://github.com/DavidAnson/markdownlint/blob/main/doc/md041.md) +* [MD047](https://github.com/DavidAnson/markdownlint/blob/main/doc/md047.md) +* [MD026](https://github.com/DavidAnson/markdownlint/blob/main/doc/md026.md) diff --git a/src/test/java/org/eolang/lints/LtInvalidMarkdownCommentTest.java b/src/test/java/org/eolang/lints/LtInvalidMarkdownCommentTest.java new file mode 100644 index 000000000..92a7c81f7 --- /dev/null +++ b/src/test/java/org/eolang/lints/LtInvalidMarkdownCommentTest.java @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package org.eolang.lints; + +import java.io.IOException; +import java.util.Map; +import matchers.DefectMatcher; +import org.eolang.parser.EoSyntax; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link LtInvalidMarkdownComment}. + * + * @since 0.0.47 + */ +final class LtInvalidMarkdownCommentTest { + @Test + void catchesMarkdownlintViolationsInComments() throws IOException { + MatcherAssert.assertThat( + "markdownlint found violation of its rules in comments", + new LtInvalidMarkdownComment().defects( + Map.of( + "test.eo", + new EoSyntax( + String.join( + "\n", + "# # Main object.", + "#", + "# This is main object of program.", + "#", + "# ### Subsection of main object.", + "#", + "# Sub information about main object.", + "[] > foo" + ) + ).parsed() + ) + ), + Matchers.allOf( + Matchers.iterableWithSize(1), + Matchers.hasItem( + Matchers.hasToString( + Matchers.allOf( + Matchers.containsString("MD001"), + Matchers.containsString(":5") + ) + ) + ), + Matchers.everyItem(new DefectMatcher()) + ) + ); + } + + @Test + void allowsCorrectMarkdownlintComments() throws IOException { + MatcherAssert.assertThat( + "markdownlint found violation of its rules in comments", + new LtInvalidMarkdownComment().defects( + Map.of( + "test.eo", + new EoSyntax( + String.join( + "\n", + "# # Main object.", + "#", + "# This is main object of program.", + "#", + "# ## Subsection of main object.", + "#", + "# Sub information about main object.", + "[] > foo" + ) + ).parsed() + ) + ), + Matchers.emptyIterable() + ); + } +}