Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 11 additions & 47 deletions src/main/java/org/eolang/lints/LtTestNotVerb.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,8 @@
import com.github.lombrozo.xnav.Xnav;
import com.jcabi.xml.XML;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import opennlp.tools.postag.POSModel;
import opennlp.tools.postag.POSTaggerME;
import org.cactoos.io.InputStreamOf;
import org.cactoos.io.ResourceOf;

/**
* Lint that checks test object name is a verb in singular.
Expand All @@ -28,43 +20,24 @@
final class LtTestNotVerb implements Lint {

/**
* The pattern to split kebab case.
* Vocabulary for English name checks.
*/
private static final Pattern KEBAB = Pattern.compile("-");

/**
* Part-Of-Speech tagger.
*/
private final POSTaggerME model;
private final Vocabulary vocabulary;

/**
* Ctor.
* @throws IOException If fails
*/
LtTestNotVerb() throws IOException {
this(
new POSModel(
new InputStreamOf(
new ResourceOf("en-pos-perceptron.bin")
)
)
);
}

/**
* Ctor.
* @param mdl Part-Of-Speech model
*/
LtTestNotVerb(final POSModel mdl) {
this(new POSTaggerME(mdl));
this(new Vocabulary());
}

/**
* Ctor.
* @param pos Part-Of-Speech tagger
* @param vocab Vocabulary to use for name checks
*/
LtTestNotVerb(final POSTaggerME pos) {
this.model = pos;
LtTestNotVerb(final Vocabulary vocab) {
this.vocabulary = vocab;
}

@Override
Expand All @@ -76,7 +49,7 @@ public String name() {
public Collection<Defect> defects(final XML xmir) throws IOException {
return new Xnav(xmir.inner())
.path("/object//o[@name and starts-with(@name, '+')]")
.filter(object -> !this.isVerbInSingular(object))
.filter(object -> !this.isVerb(object))
.map(LtTestNotVerb::verbDefect)
.collect(Collectors.toList());
}
Expand All @@ -87,22 +60,13 @@ public String motive() throws IOException {
}

/**
* Check if object name is verb in singular.
* Check if the test object name is a verb in singular.
* @param object Object navigator
* @return True if first word is a verb in singular form
*/
private boolean isVerbInSingular(final Xnav object) {
return "VBZ".equals(
this.model.tag(
Stream.concat(
Stream.of("It"),
Arrays.stream(
LtTestNotVerb.KEBAB.split(
object.attribute("name").text().get().replace("+", "")
)
)
).map(s -> s.toLowerCase(Locale.ROOT)).toArray(String[]::new)
)[1]
private boolean isVerb(final Xnav object) {
return this.vocabulary.isVerb(
object.attribute("name").text().get().replace("+", "")
);
}

Expand Down
86 changes: 86 additions & 0 deletions src/main/java/org/eolang/lints/Vocabulary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/
package org.eolang.lints;

import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import opennlp.tools.postag.POSModel;
import opennlp.tools.postag.POSTaggerME;
import org.cactoos.io.InputStreamOf;
import org.cactoos.io.ResourceOf;

/**
* English vocabulary checks for EO object names.
*
* <p>Uses <a href="https://opennlp.apache.org/">OpenNLP</a> POS tagging to
* determine the grammatical role of words in a kebab-case name.</p>
*
* @since 0.2.0
*/
final class Vocabulary {

/**
* Pattern to split kebab-case names.
*/
private static final Pattern KEBAB = Pattern.compile("-");

/**
* Part-Of-Speech tagger.
*/
private final POSTaggerME tagger;

/**
* Ctor.
* @throws IOException If fails to load the POS model resource
*/
Vocabulary() throws IOException {
this(
new POSModel(
new InputStreamOf(
new ResourceOf("en-pos-perceptron.bin")
)
)
);
}

/**
* Ctor.
* @param mdl Part-Of-Speech model
*/
Vocabulary(final POSModel mdl) {
this(new POSTaggerME(mdl));
}

/**
* Ctor.
* @param pos Part-Of-Speech tagger
*/
Vocabulary(final POSTaggerME pos) {
this.tagger = pos;
}

/**
* Check if the given kebab-case name starts with a verb in third-person singular.
*
* <p>The check uses the "It [verb]s" rule: "It generates-report" → the
* first word must be tagged {@code VBZ} (verb, 3rd-person singular present).</p>
*
* @param name Kebab-case name without any leading {@code +} sigil
* @return True if the first word is a VBZ-tagged verb
*/
boolean isVerb(final String name) {
return "VBZ".equals(
this.tagger.tag(
Stream.concat(
Stream.of("It"),
Arrays.stream(Vocabulary.KEBAB.split(name))
).map(s -> s.toLowerCase(Locale.ROOT)).toArray(String[]::new)
)[1]
);
}
}
84 changes: 6 additions & 78 deletions src/test/java/org/eolang/lints/LtTestNotVerbTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,68 +17,21 @@
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

/**
* Tests for {@link LtTestNotVerb}.
* @since 0.0.22
* @todo #872:60min Extract name-validation logic from LtTestNotVerb into a testable component.
* Currently {@link LtTestNotVerbTest} parses many EO programs that differ only in the
* object name being tested, making the tests slow and hard to maintain. The name-validation
* predicate (verb vs. non-verb check) should be extracted into its own class so it can be
* tested directly with plain strings — no EO parsing required. Once extracted, reduce
* {@link LtTestNotVerbTest} to a few representative end-to-end cases and add a dedicated
* unit test class for the new component.
*/
final class LtTestNotVerbTest {

@ExtendWith(MayBeSlow.class)
@Execution(ExecutionMode.CONCURRENT)
@ParameterizedTest
@ValueSource(
strings = {
"itIsTrue",
"testing",
"this-is-test",
"it-works",
"nothing-happened",
"something-is-wrong",
"will-fail-eventually",
"always-returns-true",
"was-lost-forever",
"nobody-knows-why",
"should-not-pass",
"once-upon-a-time",
"must-do-better",
"was-a-trap",
"dont-look-here",
"never-saw-it-coming",
"this-time-for-sure",
"there-it-goes",
"it-is-fine-probably",
"maybe-next-time",
"well-this-is-awkward",
"why-this-again",
"here-we-go-again",
"it-was-working-before",
"expected-the-unexpected",
"it-seems-fine",
"suddenly-works",
"too-late-now",
"could-not-care-less",
"it-just-works",
"dont-push-that-button",
"error-404-not-found",
"who-did-this",
"it-has-a-plan",
"will-never-finish",
"accidentally-passed",
"i-think-its-ok",
"chicken-as-expected",
"please-reboot",
"hope-it-works"
}
)
@ValueSource(strings = {"it-works", "should-not-pass", "testing"})
void catchesBadName(final String name) throws IOException {
MatcherAssert.assertThat(
"Defects size doesn't match with expected",
Expand All @@ -104,34 +57,9 @@ void catchesBadName(final String name) throws IOException {
}

@ExtendWith(MayBeSlow.class)
@Execution(ExecutionMode.CONCURRENT)
@ParameterizedTest
@ValueSource(
strings = {
"generates-report",
"locks-branch",
"parses-dom",
"prints-data",
"runs",
"works-as-expected",
"breaks-hearts",
"crashes-again",
"forgets-everything",
"has-been-found",
"looks-fine-to-me",
"returns-something-strange",
"disappears-silently",
"follows-the-rules",
"finds-nothing-at-all",
"sounds-legit",
"sleeps-forever",
"makes-zero-sense",
"runs-in-circles",
"is-never-called",
"is-kind-of-slow",
"is-totally-broken",
"is-almost-correct"
}
)
@ValueSource(strings = {"generates-report", "runs", "parses-dom"})
void allowsGoodNames(final String name) throws IOException {
MatcherAssert.assertThat(
"Defects are not empty, but they shouldn't be",
Expand Down
Loading
Loading