mirror of
https://github.com/aNNiMON/Own-Programming-Language-Tutorial.git
synced 2024-09-20 00:34:20 +03:00
Add semantic linter as a required stage
This commit is contained in:
parent
1535e86472
commit
35971e874b
@ -0,0 +1,27 @@
|
||||
package com.annimon.ownlang.util.input;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record InputSourceDetector() {
|
||||
public static final String RESOURCE_PREFIX = "resource:";
|
||||
|
||||
public boolean isReadable(String programPath) {
|
||||
if (programPath.startsWith(RESOURCE_PREFIX)) {
|
||||
String path = programPath.substring(RESOURCE_PREFIX.length());
|
||||
return getClass().getResource(path) != null;
|
||||
} else {
|
||||
Path path = Path.of(programPath);
|
||||
return Files.isReadable(path) && Files.isRegularFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
public InputSource toInputSource(String programPath) {
|
||||
if (programPath.startsWith(RESOURCE_PREFIX)) {
|
||||
String path = programPath.substring(RESOURCE_PREFIX.length());
|
||||
return new InputSourceResource(path);
|
||||
} else {
|
||||
return new InputSourceFile(programPath);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ import com.annimon.ownlang.utils.Sandbox;
|
||||
import com.annimon.ownlang.utils.TimeMeasurement;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@ -72,8 +73,13 @@ public final class Main {
|
||||
|
||||
case "-l":
|
||||
case "--lint":
|
||||
options.lintMode = true;
|
||||
return;
|
||||
final String lintMode = i + 1 < args.length ? args[++i] : LinterStage.Mode.SEMANTIC.name();
|
||||
options.lintMode = switch (lintMode.toLowerCase(Locale.ROOT)) {
|
||||
case "none" -> LinterStage.Mode.NONE;
|
||||
case "full" -> LinterStage.Mode.FULL;
|
||||
default -> LinterStage.Mode.SEMANTIC;
|
||||
};
|
||||
break;
|
||||
|
||||
case "-f":
|
||||
case "--file":
|
||||
@ -112,8 +118,8 @@ public final class Main {
|
||||
options:
|
||||
-f, --file [input] Run program file. Required.
|
||||
-r, --repl Enter to a REPL mode
|
||||
-l, --lint Find bugs in code
|
||||
-o N, --optimize N Perform optimization with N (0...9) passes
|
||||
-l, --lint <mode> Find bugs in code. Mode: none, semantic, full
|
||||
-o, --optimize N Perform optimization with N (0...9) passes
|
||||
-b, --beautify Beautify source code
|
||||
-a, --showast Show AST of program
|
||||
-t, --showtokens Show lexical tokens
|
||||
@ -145,8 +151,8 @@ public final class Main {
|
||||
.thenConditional(options.optimizationLevel > 0,
|
||||
scopedStages.create("Optimization",
|
||||
new OptimizationStage(options.optimizationLevel, options.showAst)))
|
||||
.thenConditional(options.lintMode,
|
||||
scopedStages.create("Linter", new LinterStage()))
|
||||
.thenConditional(options.linterEnabled(),
|
||||
scopedStages.create("Linter", new LinterStage(options.lintMode)))
|
||||
.then(scopedStages.create("Function adding", new FunctionAddingStage()))
|
||||
.then(scopedStages.create("Execution", new ExecutionStage()))
|
||||
.perform(stagesData, options.toInputSource());
|
||||
|
@ -1,21 +1,17 @@
|
||||
package com.annimon.ownlang;
|
||||
|
||||
import com.annimon.ownlang.util.input.InputSource;
|
||||
import com.annimon.ownlang.util.input.InputSourceFile;
|
||||
import com.annimon.ownlang.util.input.InputSourceProgram;
|
||||
import com.annimon.ownlang.util.input.InputSourceResource;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import com.annimon.ownlang.parser.linters.LinterStage;
|
||||
import com.annimon.ownlang.util.input.*;
|
||||
import static com.annimon.ownlang.util.input.InputSourceDetector.RESOURCE_PREFIX;
|
||||
|
||||
public class RunOptions {
|
||||
private static final String RESOURCE_PREFIX = "resource:";
|
||||
private static final String DEFAULT_PROGRAM = "program.own";
|
||||
|
||||
// input
|
||||
String programPath;
|
||||
String programSource;
|
||||
// modes
|
||||
boolean lintMode;
|
||||
LinterStage.Mode lintMode = LinterStage.Mode.SEMANTIC;
|
||||
boolean beautifyMode;
|
||||
int optimizationLevel;
|
||||
// flags
|
||||
@ -23,11 +19,18 @@ public class RunOptions {
|
||||
boolean showAst;
|
||||
boolean showMeasurements;
|
||||
|
||||
private final InputSourceDetector inputSourceDetector = new InputSourceDetector();
|
||||
|
||||
boolean linterEnabled() {
|
||||
return lintMode != null && lintMode != LinterStage.Mode.NONE;
|
||||
}
|
||||
|
||||
String detectDefaultProgramPath() {
|
||||
if (getClass().getResource("/" + DEFAULT_PROGRAM) != null) {
|
||||
return RESOURCE_PREFIX + "/" + DEFAULT_PROGRAM;
|
||||
final String resourcePath = RESOURCE_PREFIX + "/" + DEFAULT_PROGRAM;
|
||||
if (inputSourceDetector.isReadable(resourcePath)) {
|
||||
return resourcePath;
|
||||
}
|
||||
if (Files.isReadable(Path.of(DEFAULT_PROGRAM))) {
|
||||
if (inputSourceDetector.isReadable(DEFAULT_PROGRAM)) {
|
||||
return DEFAULT_PROGRAM;
|
||||
}
|
||||
return null;
|
||||
@ -44,12 +47,6 @@ public class RunOptions {
|
||||
throw new IllegalArgumentException("Empty input");
|
||||
}
|
||||
}
|
||||
|
||||
if (programPath.startsWith(RESOURCE_PREFIX)) {
|
||||
String path = programPath.substring(RESOURCE_PREFIX.length());
|
||||
return new InputSourceResource(path);
|
||||
} else {
|
||||
return new InputSourceFile(programPath);
|
||||
}
|
||||
return inputSourceDetector.toInputSource(programPath);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +0,0 @@
|
||||
package com.annimon.ownlang.exceptions;
|
||||
|
||||
import com.annimon.ownlang.util.Range;
|
||||
|
||||
/**
|
||||
* Base type for all lexer and parser exceptions
|
||||
*/
|
||||
public abstract class BaseParserException extends RuntimeException {
|
||||
|
||||
private final Range range;
|
||||
|
||||
public BaseParserException(String message, Range range) {
|
||||
super(message);
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public Range getRange() {
|
||||
return range;
|
||||
}
|
||||
}
|
@ -1,27 +1,27 @@
|
||||
package com.annimon.ownlang.exceptions;
|
||||
|
||||
import com.annimon.ownlang.parser.error.ParseError;
|
||||
import com.annimon.ownlang.parser.error.ParseErrors;
|
||||
import com.annimon.ownlang.util.SourceLocatedError;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Single Exception for Lexer and Parser errors
|
||||
* Single Exception for Lexer, Parser and Linter errors
|
||||
*/
|
||||
public class OwnLangParserException extends RuntimeException {
|
||||
|
||||
private final ParseErrors parseErrors;
|
||||
private final Collection<? extends SourceLocatedError> errors;
|
||||
|
||||
public OwnLangParserException(ParseError parseError) {
|
||||
super(parseError.toString());
|
||||
this.parseErrors = new ParseErrors();
|
||||
parseErrors.add(parseError);;
|
||||
public OwnLangParserException(SourceLocatedError error) {
|
||||
super(error.toString());
|
||||
errors = List.of(error);;
|
||||
}
|
||||
|
||||
public OwnLangParserException(ParseErrors parseErrors) {
|
||||
super(parseErrors.toString());
|
||||
this.parseErrors = parseErrors;
|
||||
public OwnLangParserException(Collection<? extends SourceLocatedError> errors) {
|
||||
super(errors.toString());
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public ParseErrors getParseErrors() {
|
||||
return parseErrors;
|
||||
public Collection<? extends SourceLocatedError> getParseErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
@ -6,13 +6,16 @@ import com.annimon.ownlang.util.Range;
|
||||
*
|
||||
* @author aNNiMON
|
||||
*/
|
||||
public final class ParseException extends BaseParserException {
|
||||
public final class ParseException extends RuntimeException {
|
||||
|
||||
public ParseException(String message) {
|
||||
super(message, Range.ZERO);
|
||||
}
|
||||
private final Range range;
|
||||
|
||||
public ParseException(String message, Range range) {
|
||||
super(message, range);
|
||||
super(message);
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public Range getRange() {
|
||||
return range;
|
||||
}
|
||||
}
|
@ -1,11 +1,12 @@
|
||||
package com.annimon.ownlang.parser.error;
|
||||
|
||||
import com.annimon.ownlang.Console;
|
||||
import java.util.AbstractList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public final class ParseErrors implements Iterable<ParseError> {
|
||||
public final class ParseErrors extends AbstractList<ParseError> {
|
||||
|
||||
private final List<ParseError> errors;
|
||||
|
||||
@ -16,11 +17,17 @@ public final class ParseErrors implements Iterable<ParseError> {
|
||||
public void clear() {
|
||||
errors.clear();
|
||||
}
|
||||
|
||||
public void add(ParseError parseError) {
|
||||
errors.add(parseError);
|
||||
|
||||
@Override
|
||||
public boolean add(ParseError parseError) {
|
||||
return errors.add(parseError);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ParseError get(int index) {
|
||||
return errors.get(index);
|
||||
}
|
||||
|
||||
public boolean hasErrors() {
|
||||
return !errors.isEmpty();
|
||||
}
|
||||
@ -30,6 +37,11 @@ public final class ParseErrors implements Iterable<ParseError> {
|
||||
return errors.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return errors.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
|
@ -4,11 +4,13 @@ import com.annimon.ownlang.stages.Stage;
|
||||
import com.annimon.ownlang.stages.StagesData;
|
||||
import com.annimon.ownlang.util.ErrorsLocationFormatterStage;
|
||||
import com.annimon.ownlang.util.ErrorsStackTraceFormatterStage;
|
||||
import com.annimon.ownlang.util.SourceLocatedError;
|
||||
import java.util.Collection;
|
||||
|
||||
public class ParseErrorsFormatterStage implements Stage<ParseErrors, String> {
|
||||
public class ParseErrorsFormatterStage implements Stage<Collection<? extends SourceLocatedError>, String> {
|
||||
|
||||
@Override
|
||||
public String perform(StagesData stagesData, ParseErrors input) {
|
||||
public String perform(StagesData stagesData, Collection<? extends SourceLocatedError> input) {
|
||||
String error = new ErrorsLocationFormatterStage()
|
||||
.perform(stagesData, input);
|
||||
String stackTrace = new ErrorsStackTraceFormatterStage()
|
||||
|
@ -1,8 +1,6 @@
|
||||
package com.annimon.ownlang.parser.linters;
|
||||
|
||||
import com.annimon.ownlang.Console;
|
||||
import com.annimon.ownlang.parser.ast.*;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@ -14,7 +12,7 @@ final class AssignValidator extends LintVisitor {
|
||||
|
||||
private final Set<String> moduleConstants = new HashSet<>();
|
||||
|
||||
AssignValidator(Collection<LinterResult> results) {
|
||||
AssignValidator(LinterResults results) {
|
||||
super(results);
|
||||
}
|
||||
|
||||
@ -24,8 +22,8 @@ final class AssignValidator extends LintVisitor {
|
||||
if (s.target instanceof VariableExpression varExpr) {
|
||||
final String variable = varExpr.name;
|
||||
if (moduleConstants.contains(variable)) {
|
||||
results.add(new LinterResult(LinterResult.Severity.WARNING,
|
||||
String.format("Variable \"%s\" overrides constant", variable)));
|
||||
results.add(LinterResult.warning(
|
||||
"Variable \"%s\" overrides constant".formatted(variable)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.annimon.ownlang.parser.linters;
|
||||
|
||||
import com.annimon.ownlang.parser.ast.*;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@ -9,7 +8,7 @@ final class DefaultFunctionsOverrideValidator extends LintVisitor {
|
||||
|
||||
private final Set<String> moduleFunctions = new HashSet<>();
|
||||
|
||||
DefaultFunctionsOverrideValidator(Collection<LinterResult> results) {
|
||||
DefaultFunctionsOverrideValidator(LinterResults results) {
|
||||
super(results);
|
||||
}
|
||||
|
||||
@ -17,8 +16,8 @@ final class DefaultFunctionsOverrideValidator extends LintVisitor {
|
||||
public void visit(FunctionDefineStatement s) {
|
||||
super.visit(s);
|
||||
if (moduleFunctions.contains(s.name)) {
|
||||
results.add(new LinterResult(LinterResult.Severity.WARNING,
|
||||
String.format("Function \"%s\" overrides default module function", s.name)));
|
||||
results.add(LinterResult.warning(
|
||||
"Function \"%s\" overrides default module function".formatted(s.name)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
package com.annimon.ownlang.parser.linters;
|
||||
|
||||
import com.annimon.ownlang.parser.ast.IncludeStatement;
|
||||
import com.annimon.ownlang.parser.ast.ValueExpression;
|
||||
import com.annimon.ownlang.util.input.InputSourceDetector;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author aNNiMON
|
||||
*/
|
||||
final class IncludeSourceValidator extends LintVisitor {
|
||||
private final InputSourceDetector detector;
|
||||
|
||||
IncludeSourceValidator(LinterResults results) {
|
||||
super(results);
|
||||
detector = new InputSourceDetector();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(IncludeStatement s) {
|
||||
super.visit(s);
|
||||
if (s.expression instanceof ValueExpression expr) {
|
||||
final String path = expr.eval().asString();
|
||||
if (!detector.isReadable(path)) {
|
||||
results.add(LinterResult.error(
|
||||
"Include statement path \"%s\" is not readable".formatted(path)));
|
||||
}
|
||||
} else {
|
||||
results.add(LinterResult.warning(
|
||||
"Include statement path \"%s\" is not a constant string".formatted(s.expression)));
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,11 @@ import com.annimon.ownlang.parser.ast.Node;
|
||||
import com.annimon.ownlang.parser.ast.Visitor;
|
||||
import com.annimon.ownlang.parser.visitors.AbstractVisitor;
|
||||
import com.annimon.ownlang.parser.visitors.VisitorUtils;
|
||||
import java.util.Collection;
|
||||
|
||||
abstract class LintVisitor extends AbstractVisitor {
|
||||
protected final Collection<LinterResult> results;
|
||||
protected final LinterResults results;
|
||||
|
||||
LintVisitor(Collection<LinterResult> results) {
|
||||
LintVisitor(LinterResults results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,42 @@
|
||||
package com.annimon.ownlang.parser.linters;
|
||||
|
||||
record LinterResult(Severity severity, String message) {
|
||||
import com.annimon.ownlang.util.Range;
|
||||
import com.annimon.ownlang.util.SourceLocatedError;
|
||||
|
||||
record LinterResult(Severity severity, String message, Range range) implements SourceLocatedError {
|
||||
|
||||
enum Severity { ERROR, WARNING }
|
||||
|
||||
static LinterResult warning(String message) {
|
||||
return new LinterResult(Severity.WARNING, message);
|
||||
}
|
||||
|
||||
static LinterResult error(String message) {
|
||||
return new LinterResult(Severity.ERROR, message);
|
||||
}
|
||||
|
||||
LinterResult(Severity severity, String message) {
|
||||
this(severity, message, null);
|
||||
}
|
||||
|
||||
public boolean isError() {
|
||||
return severity == Severity.ERROR;
|
||||
}
|
||||
|
||||
public boolean isWarning() {
|
||||
return severity == Severity.WARNING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range getRange() {
|
||||
return range;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return severity.name() + ": " + message;
|
||||
|
@ -0,0 +1,55 @@
|
||||
package com.annimon.ownlang.parser.linters;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
class LinterResults extends AbstractList<LinterResult> {
|
||||
private final List<LinterResult> results;
|
||||
|
||||
LinterResults() {
|
||||
this(new ArrayList<>());
|
||||
}
|
||||
|
||||
LinterResults(List<LinterResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean add(LinterResult result) {
|
||||
return results.add(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LinterResult get(int index) {
|
||||
return results.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<LinterResult> iterator() {
|
||||
return results.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return results.size();
|
||||
}
|
||||
|
||||
public boolean hasErrors() {
|
||||
return results.stream().anyMatch(LinterResult::isError);
|
||||
}
|
||||
|
||||
public Stream<LinterResult> errors() {
|
||||
return results.stream().filter(LinterResult::isError);
|
||||
}
|
||||
|
||||
public boolean hasWarnings() {
|
||||
return results.stream().anyMatch(LinterResult::isWarning);
|
||||
}
|
||||
|
||||
public Stream<LinterResult> warnings() {
|
||||
return results.stream().filter(LinterResult::isWarning);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.annimon.ownlang.parser.linters;
|
||||
|
||||
import com.annimon.ownlang.Console;
|
||||
import com.annimon.ownlang.exceptions.OwnLangParserException;
|
||||
import com.annimon.ownlang.lib.ScopeHandler;
|
||||
import com.annimon.ownlang.parser.ast.Node;
|
||||
import com.annimon.ownlang.parser.ast.Visitor;
|
||||
@ -11,23 +12,42 @@ import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class LinterStage implements Stage<Node, Node> {
|
||||
public enum Mode { NONE, SEMANTIC, FULL }
|
||||
|
||||
private final Mode mode;
|
||||
|
||||
public LinterStage(Mode mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node perform(StagesData stagesData, Node input) {
|
||||
final List<LinterResult> results = new ArrayList<>();
|
||||
final Visitor[] validators = new Visitor[] {
|
||||
new AssignValidator(results),
|
||||
new DefaultFunctionsOverrideValidator(results)
|
||||
};
|
||||
if (mode == Mode.NONE) return input;
|
||||
|
||||
ScopeHandler.resetScope();
|
||||
final LinterResults results = new LinterResults();
|
||||
final List<Visitor> validators = new ArrayList<>();
|
||||
validators.add(new IncludeSourceValidator(results));
|
||||
|
||||
if (mode == Mode.SEMANTIC) {
|
||||
validators.forEach(input::accept);
|
||||
if (results.hasErrors()) {
|
||||
throw new OwnLangParserException(results.errors().toList());
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
// Full lint validation with Console output
|
||||
validators.add(new AssignValidator(results));
|
||||
validators.add(new DefaultFunctionsOverrideValidator(results));
|
||||
|
||||
ScopeHandler.resetScope(); // TODO special linter scope?
|
||||
for (Visitor validator : validators) {
|
||||
input.accept(validator);
|
||||
ScopeHandler.resetScope();
|
||||
}
|
||||
|
||||
results.sort(Comparator.comparing(LinterResult::severity));
|
||||
Console.println(String.format("Lint validation completed. %d results found!", results.size()));
|
||||
Console.println("Lint validation completed. %d results found!".formatted(results.size()));
|
||||
for (LinterResult r : results) {
|
||||
switch (r.severity()) {
|
||||
case ERROR -> Console.error(r.toString());
|
||||
|
@ -9,6 +9,7 @@ import com.annimon.ownlang.parser.ast.FunctionDefineStatement;
|
||||
import com.annimon.ownlang.parser.ast.Node;
|
||||
import com.annimon.ownlang.parser.ast.Visitor;
|
||||
import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage;
|
||||
import com.annimon.ownlang.parser.linters.LinterStage;
|
||||
import com.annimon.ownlang.parser.optimization.OptimizationStage;
|
||||
import com.annimon.ownlang.parser.visitors.AbstractVisitor;
|
||||
import com.annimon.ownlang.stages.*;
|
||||
@ -16,7 +17,6 @@ import com.annimon.ownlang.util.input.InputSource;
|
||||
import com.annimon.ownlang.util.input.InputSourceFile;
|
||||
import com.annimon.ownlang.util.input.SourceLoaderStage;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import java.io.File;
|
||||
@ -39,7 +39,9 @@ public class ProgramsTest {
|
||||
testPipeline = new SourceLoaderStage()
|
||||
.then(new LexerStage())
|
||||
.then(new ParserStage())
|
||||
.then(new LinterStage(LinterStage.Mode.SEMANTIC))
|
||||
.thenConditional(true, new OptimizationStage(9))
|
||||
.then(ProgramsTest::mockOUnit)
|
||||
.then(new ExecutionStage())
|
||||
.then((stagesData, input) -> {
|
||||
input.accept(testFunctionsExecutor);
|
||||
@ -47,8 +49,7 @@ public class ProgramsTest {
|
||||
});
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void initialize() {
|
||||
private static Node mockOUnit(StagesData stagesData, Node input) {
|
||||
ScopeHandler.resetScope();
|
||||
// Let's mock junit methods as ounit functions
|
||||
ScopeHandler.setFunction("assertEquals", (args) -> {
|
||||
@ -84,6 +85,7 @@ public class ProgramsTest {
|
||||
}
|
||||
return NumberValue.ONE;
|
||||
});
|
||||
return input;
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
|
Loading…
Reference in New Issue
Block a user