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 com.annimon.ownlang.utils.TimeMeasurement;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -72,8 +73,13 @@ public final class Main {
case "-l": case "-l":
case "--lint": case "--lint":
options.lintMode = true; final String lintMode = i + 1 < args.length ? args[++i] : LinterStage.Mode.SEMANTIC.name();
return; 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 "-f":
case "--file": case "--file":
@ -112,8 +118,8 @@ public final class Main {
options: options:
-f, --file [input] Run program file. Required. -f, --file [input] Run program file. Required.
-r, --repl Enter to a REPL mode -r, --repl Enter to a REPL mode
-l, --lint Find bugs in code -l, --lint <mode> Find bugs in code. Mode: none, semantic, full
-o N, --optimize N Perform optimization with N (0...9) passes -o, --optimize N Perform optimization with N (0...9) passes
-b, --beautify Beautify source code -b, --beautify Beautify source code
-a, --showast Show AST of program -a, --showast Show AST of program
-t, --showtokens Show lexical tokens -t, --showtokens Show lexical tokens
@ -145,8 +151,8 @@ public final class Main {
.thenConditional(options.optimizationLevel > 0, .thenConditional(options.optimizationLevel > 0,
scopedStages.create("Optimization", scopedStages.create("Optimization",
new OptimizationStage(options.optimizationLevel, options.showAst))) new OptimizationStage(options.optimizationLevel, options.showAst)))
.thenConditional(options.lintMode, .thenConditional(options.linterEnabled(),
scopedStages.create("Linter", new LinterStage())) scopedStages.create("Linter", new LinterStage(options.lintMode)))
.then(scopedStages.create("Function adding", new FunctionAddingStage())) .then(scopedStages.create("Function adding", new FunctionAddingStage()))
.then(scopedStages.create("Execution", new ExecutionStage())) .then(scopedStages.create("Execution", new ExecutionStage()))
.perform(stagesData, options.toInputSource()); .perform(stagesData, options.toInputSource());

View File

@ -1,21 +1,17 @@
package com.annimon.ownlang; package com.annimon.ownlang;
import com.annimon.ownlang.util.input.InputSource; import com.annimon.ownlang.parser.linters.LinterStage;
import com.annimon.ownlang.util.input.InputSourceFile; import com.annimon.ownlang.util.input.*;
import com.annimon.ownlang.util.input.InputSourceProgram; import static com.annimon.ownlang.util.input.InputSourceDetector.RESOURCE_PREFIX;
import com.annimon.ownlang.util.input.InputSourceResource;
import java.nio.file.Files;
import java.nio.file.Path;
public class RunOptions { public class RunOptions {
private static final String RESOURCE_PREFIX = "resource:";
private static final String DEFAULT_PROGRAM = "program.own"; private static final String DEFAULT_PROGRAM = "program.own";
// input // input
String programPath; String programPath;
String programSource; String programSource;
// modes // modes
boolean lintMode; LinterStage.Mode lintMode = LinterStage.Mode.SEMANTIC;
boolean beautifyMode; boolean beautifyMode;
int optimizationLevel; int optimizationLevel;
// flags // flags
@ -23,11 +19,18 @@ public class RunOptions {
boolean showAst; boolean showAst;
boolean showMeasurements; boolean showMeasurements;
String detectDefaultProgramPath() { private final InputSourceDetector inputSourceDetector = new InputSourceDetector();
if (getClass().getResource("/" + DEFAULT_PROGRAM) != null) {
return RESOURCE_PREFIX + "/" + DEFAULT_PROGRAM; boolean linterEnabled() {
return lintMode != null && lintMode != LinterStage.Mode.NONE;
} }
if (Files.isReadable(Path.of(DEFAULT_PROGRAM))) {
String detectDefaultProgramPath() {
final String resourcePath = RESOURCE_PREFIX + "/" + DEFAULT_PROGRAM;
if (inputSourceDetector.isReadable(resourcePath)) {
return resourcePath;
}
if (inputSourceDetector.isReadable(DEFAULT_PROGRAM)) {
return DEFAULT_PROGRAM; return DEFAULT_PROGRAM;
} }
return null; return null;
@ -44,12 +47,6 @@ public class RunOptions {
throw new IllegalArgumentException("Empty input"); throw new IllegalArgumentException("Empty input");
} }
} }
return inputSourceDetector.toInputSource(programPath);
if (programPath.startsWith(RESOURCE_PREFIX)) {
String path = programPath.substring(RESOURCE_PREFIX.length());
return new InputSourceResource(path);
} else {
return new InputSourceFile(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; package com.annimon.ownlang.exceptions;
import com.annimon.ownlang.parser.error.ParseError; import com.annimon.ownlang.util.SourceLocatedError;
import com.annimon.ownlang.parser.error.ParseErrors; 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 { public class OwnLangParserException extends RuntimeException {
private final ParseErrors parseErrors; private final Collection<? extends SourceLocatedError> errors;
public OwnLangParserException(ParseError parseError) { public OwnLangParserException(SourceLocatedError error) {
super(parseError.toString()); super(error.toString());
this.parseErrors = new ParseErrors(); errors = List.of(error);;
parseErrors.add(parseError);;
} }
public OwnLangParserException(ParseErrors parseErrors) { public OwnLangParserException(Collection<? extends SourceLocatedError> errors) {
super(parseErrors.toString()); super(errors.toString());
this.parseErrors = parseErrors; this.errors = errors;
} }
public ParseErrors getParseErrors() { public Collection<? extends SourceLocatedError> getParseErrors() {
return parseErrors; return errors;
} }
} }

View File

@ -6,13 +6,16 @@ import com.annimon.ownlang.util.Range;
* *
* @author aNNiMON * @author aNNiMON
*/ */
public final class ParseException extends BaseParserException { public final class ParseException extends RuntimeException {
public ParseException(String message) { private final Range range;
super(message, Range.ZERO);
}
public ParseException(String message, 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; package com.annimon.ownlang.parser.error;
import com.annimon.ownlang.Console; import com.annimon.ownlang.Console;
import java.util.AbstractList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
public final class ParseErrors implements Iterable<ParseError> { public final class ParseErrors extends AbstractList<ParseError> {
private final List<ParseError> errors; private final List<ParseError> errors;
@ -17,8 +18,14 @@ public final class ParseErrors implements Iterable<ParseError> {
errors.clear(); errors.clear();
} }
public void add(ParseError parseError) { @Override
errors.add(parseError); public boolean add(ParseError parseError) {
return errors.add(parseError);
}
@Override
public ParseError get(int index) {
return errors.get(index);
} }
public boolean hasErrors() { public boolean hasErrors() {
@ -30,6 +37,11 @@ public final class ParseErrors implements Iterable<ParseError> {
return errors.iterator(); return errors.iterator();
} }
@Override
public int size() {
return errors.size();
}
@Override @Override
public String toString() { public String toString() {
final StringBuilder result = new StringBuilder(); 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.stages.StagesData;
import com.annimon.ownlang.util.ErrorsLocationFormatterStage; import com.annimon.ownlang.util.ErrorsLocationFormatterStage;
import com.annimon.ownlang.util.ErrorsStackTraceFormatterStage; 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 @Override
public String perform(StagesData stagesData, ParseErrors input) { public String perform(StagesData stagesData, Collection<? extends SourceLocatedError> input) {
String error = new ErrorsLocationFormatterStage() String error = new ErrorsLocationFormatterStage()
.perform(stagesData, input); .perform(stagesData, input);
String stackTrace = new ErrorsStackTraceFormatterStage() String stackTrace = new ErrorsStackTraceFormatterStage()

View File

@ -1,8 +1,6 @@
package com.annimon.ownlang.parser.linters; package com.annimon.ownlang.parser.linters;
import com.annimon.ownlang.Console;
import com.annimon.ownlang.parser.ast.*; import com.annimon.ownlang.parser.ast.*;
import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -14,7 +12,7 @@ final class AssignValidator extends LintVisitor {
private final Set<String> moduleConstants = new HashSet<>(); private final Set<String> moduleConstants = new HashSet<>();
AssignValidator(Collection<LinterResult> results) { AssignValidator(LinterResults results) {
super(results); super(results);
} }
@ -24,8 +22,8 @@ final class AssignValidator extends LintVisitor {
if (s.target instanceof VariableExpression varExpr) { if (s.target instanceof VariableExpression varExpr) {
final String variable = varExpr.name; final String variable = varExpr.name;
if (moduleConstants.contains(variable)) { if (moduleConstants.contains(variable)) {
results.add(new LinterResult(LinterResult.Severity.WARNING, results.add(LinterResult.warning(
String.format("Variable \"%s\" overrides constant", variable))); "Variable \"%s\" overrides constant".formatted(variable)));
} }
} }
} }

View File

@ -1,7 +1,6 @@
package com.annimon.ownlang.parser.linters; package com.annimon.ownlang.parser.linters;
import com.annimon.ownlang.parser.ast.*; import com.annimon.ownlang.parser.ast.*;
import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@ -9,7 +8,7 @@ final class DefaultFunctionsOverrideValidator extends LintVisitor {
private final Set<String> moduleFunctions = new HashSet<>(); private final Set<String> moduleFunctions = new HashSet<>();
DefaultFunctionsOverrideValidator(Collection<LinterResult> results) { DefaultFunctionsOverrideValidator(LinterResults results) {
super(results); super(results);
} }
@ -17,8 +16,8 @@ final class DefaultFunctionsOverrideValidator extends LintVisitor {
public void visit(FunctionDefineStatement s) { public void visit(FunctionDefineStatement s) {
super.visit(s); super.visit(s);
if (moduleFunctions.contains(s.name)) { if (moduleFunctions.contains(s.name)) {
results.add(new LinterResult(LinterResult.Severity.WARNING, results.add(LinterResult.warning(
String.format("Function \"%s\" overrides default module function", s.name))); "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.ast.Visitor;
import com.annimon.ownlang.parser.visitors.AbstractVisitor; import com.annimon.ownlang.parser.visitors.AbstractVisitor;
import com.annimon.ownlang.parser.visitors.VisitorUtils; import com.annimon.ownlang.parser.visitors.VisitorUtils;
import java.util.Collection;
abstract class LintVisitor extends AbstractVisitor { abstract class LintVisitor extends AbstractVisitor {
protected final Collection<LinterResult> results; protected final LinterResults results;
LintVisitor(Collection<LinterResult> results) { LintVisitor(LinterResults results) {
this.results = results; this.results = results;
} }

View File

@ -1,9 +1,42 @@
package com.annimon.ownlang.parser.linters; 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 } 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 @Override
public String toString() { public String toString() {
return severity.name() + ": " + message; 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; package com.annimon.ownlang.parser.linters;
import com.annimon.ownlang.Console; import com.annimon.ownlang.Console;
import com.annimon.ownlang.exceptions.OwnLangParserException;
import com.annimon.ownlang.lib.ScopeHandler; import com.annimon.ownlang.lib.ScopeHandler;
import com.annimon.ownlang.parser.ast.Node; import com.annimon.ownlang.parser.ast.Node;
import com.annimon.ownlang.parser.ast.Visitor; import com.annimon.ownlang.parser.ast.Visitor;
@ -11,23 +12,42 @@ import java.util.Comparator;
import java.util.List; import java.util.List;
public class LinterStage implements Stage<Node, Node> { 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 @Override
public Node perform(StagesData stagesData, Node input) { public Node perform(StagesData stagesData, Node input) {
final List<LinterResult> results = new ArrayList<>(); if (mode == Mode.NONE) return input;
final Visitor[] validators = new Visitor[] {
new AssignValidator(results),
new DefaultFunctionsOverrideValidator(results)
};
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) { for (Visitor validator : validators) {
input.accept(validator); input.accept(validator);
ScopeHandler.resetScope(); ScopeHandler.resetScope();
} }
results.sort(Comparator.comparing(LinterResult::severity)); 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) { for (LinterResult r : results) {
switch (r.severity()) { switch (r.severity()) {
case ERROR -> Console.error(r.toString()); 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.Node;
import com.annimon.ownlang.parser.ast.Visitor; import com.annimon.ownlang.parser.ast.Visitor;
import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage; 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.optimization.OptimizationStage;
import com.annimon.ownlang.parser.visitors.AbstractVisitor; import com.annimon.ownlang.parser.visitors.AbstractVisitor;
import com.annimon.ownlang.stages.*; 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.InputSourceFile;
import com.annimon.ownlang.util.input.SourceLoaderStage; import com.annimon.ownlang.util.input.SourceLoaderStage;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import java.io.File; import java.io.File;
@ -39,7 +39,9 @@ public class ProgramsTest {
testPipeline = new SourceLoaderStage() testPipeline = new SourceLoaderStage()
.then(new LexerStage()) .then(new LexerStage())
.then(new ParserStage()) .then(new ParserStage())
.then(new LinterStage(LinterStage.Mode.SEMANTIC))
.thenConditional(true, new OptimizationStage(9)) .thenConditional(true, new OptimizationStage(9))
.then(ProgramsTest::mockOUnit)
.then(new ExecutionStage()) .then(new ExecutionStage())
.then((stagesData, input) -> { .then((stagesData, input) -> {
input.accept(testFunctionsExecutor); input.accept(testFunctionsExecutor);
@ -47,8 +49,7 @@ public class ProgramsTest {
}); });
} }
@BeforeEach private static Node mockOUnit(StagesData stagesData, Node input) {
public void initialize() {
ScopeHandler.resetScope(); ScopeHandler.resetScope();
// Let's mock junit methods as ounit functions // Let's mock junit methods as ounit functions
ScopeHandler.setFunction("assertEquals", (args) -> { ScopeHandler.setFunction("assertEquals", (args) -> {
@ -84,6 +85,7 @@ public class ProgramsTest {
} }
return NumberValue.ONE; return NumberValue.ONE;
}); });
return input;
} }
@ParameterizedTest @ParameterizedTest