Add semantic linter as a required stage

This commit is contained in:
aNNiMON 2023-10-15 22:47:39 +03:00 committed by Victor Melnik
parent 1535e86472
commit 35971e874b
16 changed files with 258 additions and 92 deletions

View File

@ -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);
}
}
}

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
@ -17,8 +18,14 @@ public final class ParseErrors implements Iterable<ParseError> {
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() {
@ -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();

View File

@ -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()

View File

@ -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)));
}
}
}

View File

@ -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)));
}
}

View File

@ -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)));
}
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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());

View File

@ -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