diff --git a/README.md b/README.md index ef4e3ad..ffc73df 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ public class FindBugsTest { .because("It's checked and OK like this", In.classes(DependencyRules.class, PmdRuleset.class).ignore("DP_DO_INSIDE_DO_PRIVILEGED"), In.classes("*Test", "Rulesets") - .and(In.classes("ClassFileParser").withMethods("parse")) + .and(In.classes("ClassFileParser").withMethods("doParse")) .ignore("URF_UNREAD_FIELD")); FindBugsResult result = new FindBugsAnalyzer(config, collector).analyze(); diff --git a/code-assert-gui/src/main/java/guru/nidi/codeassert/gui/AppController.java b/code-assert-gui/src/main/java/guru/nidi/codeassert/gui/AppController.java index eff9b71..9e1f4f2 100644 --- a/code-assert-gui/src/main/java/guru/nidi/codeassert/gui/AppController.java +++ b/code-assert-gui/src/main/java/guru/nidi/codeassert/gui/AppController.java @@ -16,6 +16,7 @@ package guru.nidi.codeassert.gui; import guru.nidi.codeassert.model.Model; +import guru.nidi.codeassert.model.ModelBuilder; import org.springframework.web.bind.annotation.*; import java.io.File; @@ -25,6 +26,6 @@ public class AppController { @GetMapping("model") public Model model(@RequestParam String jarfile) { - return Model.from(new File(jarfile)); + return new ModelBuilder().files(new File(jarfile)).build(); } } diff --git a/code-assert/pom.xml b/code-assert/pom.xml index c2fd625..22cacda 100644 --- a/code-assert/pom.xml +++ b/code-assert/pom.xml @@ -65,7 +65,7 @@ kotlin-maven-plugin org.jetbrains.kotlin - 1.1.50 + 1.1.61 test-compile diff --git a/code-assert/src/main/java/guru/nidi/codeassert/dependency/DependencyAnalyzer.java b/code-assert/src/main/java/guru/nidi/codeassert/dependency/DependencyAnalyzer.java index 6d4fae0..99bef96 100644 --- a/code-assert/src/main/java/guru/nidi/codeassert/dependency/DependencyAnalyzer.java +++ b/code-assert/src/main/java/guru/nidi/codeassert/dependency/DependencyAnalyzer.java @@ -17,8 +17,7 @@ import guru.nidi.codeassert.Analyzer; import guru.nidi.codeassert.config.*; -import guru.nidi.codeassert.model.Model; -import guru.nidi.codeassert.model.Scope; +import guru.nidi.codeassert.model.*; import java.util.*; @@ -33,7 +32,8 @@ public class DependencyAnalyzer implements Analyzer { private final DependencyCollector collector; public DependencyAnalyzer(AnalyzerConfig config) { - this(Model.from(config.getClasses()), DependencyRules.denyAll(), Scope.PACKAGES, new DependencyCollector()); + this(new ModelBuilder().files(config.getClasses()).build(), + DependencyRules.denyAll(), Scope.PACKAGES, new DependencyCollector()); } public DependencyAnalyzer(Model model) { diff --git a/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClass.java b/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClass.java index 047420d..34bcfc0 100755 --- a/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClass.java +++ b/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClass.java @@ -35,14 +35,12 @@ public class CodeClass extends UsingElement { private final Set annotations; final List fields = new ArrayList<>(); final List methods = new ArrayList<>(); + SourceFile source; + boolean analyzed; String sourceFile; int codeSize; int totalSize; boolean concrete; - int codeLines; - int commentLines; - int emptyLines; - int totalLines; CodeClass(String name, CodePackage pack) { this.name = name; @@ -65,6 +63,10 @@ public String getSourceFile() { return sourceFile; } + public SourceFile getSource() { + return source; + } + public Set getAnnotations() { return annotations; } @@ -89,22 +91,6 @@ public boolean isConcrete() { return concrete; } - public int getCodeLines() { - return codeLines; - } - - public int getCommentLines() { - return commentLines; - } - - public int getEmptyLines() { - return emptyLines; - } - - public int getTotalLines() { - return totalLines; - } - @Override public CodeClass self() { return this; diff --git a/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClassBuilder.java b/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClassBuilder.java index 57242de..a8af652 100755 --- a/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClassBuilder.java +++ b/code-assert/src/main/java/guru/nidi/codeassert/model/CodeClassBuilder.java @@ -31,6 +31,7 @@ class CodeClassBuilder { private CodeClassBuilder(CodeClass clazz, Model model, ConstantPool constantPool) { this.clazz = clazz; + this.clazz.analyzed = true; this.model = model; this.constantPool = constantPool; } @@ -115,14 +116,6 @@ public CodeClassBuilder addCodeSizes(int totalSize, List methods) { return this; } - public CodeClassBuilder addSourceSizes(int codeLines, int commentLines, int emptyLines, int totalLines) { - clazz.codeLines = codeLines; - clazz.commentLines = commentLines; - clazz.emptyLines = emptyLines; - clazz.totalLines = totalLines; - return this; - } - private void addMemberAnnotationRefs(List infos) throws IOException { for (final MemberInfo info : infos) { if (info.annotations != null) { diff --git a/code-assert/src/main/java/guru/nidi/codeassert/model/Model.java b/code-assert/src/main/java/guru/nidi/codeassert/model/Model.java index ff508c3..8c362ca 100644 --- a/code-assert/src/main/java/guru/nidi/codeassert/model/Model.java +++ b/code-assert/src/main/java/guru/nidi/codeassert/model/Model.java @@ -15,64 +15,14 @@ */ package guru.nidi.codeassert.model; -import guru.nidi.codeassert.AnalyzerException; - -import java.io.*; import java.util.*; -import java.util.jar.JarInputStream; -import java.util.zip.ZipEntry; - -import static java.util.Arrays.asList; public class Model { public static final String UNNAMED_PACKAGE = ""; final Map packages = new HashMap<>(); final Map classes = new HashMap<>(); - - public static Model from(File... files) { - return from(asList(files)); - } - - public static Model from(List files) { - return new Model().and(files); - } - - public Model and(File... files) { - return and(asList(files)); - } - - public Model and(List files) { - try { - final ClassFileParser classParser = new ClassFileParser(); - for (final File file : files) { - try (final InputStream in = new FileInputStream(file)) { - add(classParser, file.getName(), in); - } - } - return this; - } catch (IOException e) { - throw new AnalyzerException("Problem creating a Model", e); - } - } - - private void add(ClassFileParser parser, String name, InputStream in) throws IOException { - if (name.endsWith(".jar") || name.endsWith(".zip") || name.endsWith(".war") || name.endsWith(".ear")) { - final JarInputStream jar = new JarInputStream(in); - ZipEntry entry; - while ((entry = jar.getNextEntry()) != null) { - try { - if (!entry.isDirectory()) { - add(parser, entry.getName(), jar); - } - } finally { - jar.closeEntry(); - } - } - } else if (name.endsWith(".class")) { - parser.parse(in, this); - } - } + final Map sources = new HashMap<>(); CodePackage getOrCreatePackage(String name) { CodePackage pack = packages.get(name); @@ -107,4 +57,7 @@ public Collection getClasses() { return classes.values(); } + public Collection getSources() { + return sources.values(); + } } diff --git a/code-assert/src/main/java/guru/nidi/codeassert/model/ModelBuilder.java b/code-assert/src/main/java/guru/nidi/codeassert/model/ModelBuilder.java new file mode 100644 index 0000000..747c651 --- /dev/null +++ b/code-assert/src/main/java/guru/nidi/codeassert/model/ModelBuilder.java @@ -0,0 +1,114 @@ +/* + * Copyright © 2015 Stefan Niederhauser (nidin@gmx.ch) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package guru.nidi.codeassert.model; + +import guru.nidi.codeassert.AnalyzerException; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.jar.JarInputStream; +import java.util.zip.ZipEntry; + +import static java.util.Arrays.asList; + +public class ModelBuilder { + private final ClassFileParser classParser = new ClassFileParser(); + private final List files = new ArrayList<>(); + private final Model model = new Model(); + + private final Map> classesBySource = new HashMap<>(); + + public ModelBuilder files(File... files) { + return files(asList(files)); + } + + public ModelBuilder files(List files) { + this.files.addAll(files); + return this; + } + + public Model build() { + try { + for (final File file : files) { + if (isZip(file.getName()) || isClass(file.getName())) { + try (final InputStream in = new FileInputStream(file)) { + parseClass(file.getName(), in); + } + } + } + for (final File file : files) { + if (!isZip(file.getName()) && !isClass(file.getName())) { + parseSource(file); + } + } + return model; + } catch (IOException e) { + throw new AnalyzerException("Problem creating a Model", e); + } + } + + private void parseSource(File file) throws IOException { + final SourceFile source = SourceFileParser.parse(file, StandardCharsets.UTF_8); + if (source != null) { + model.sources.put(file.getName(), source); + final List classes = classesBySource.get(file.getName()); + if (classes != null) { + source.classes.addAll(classes); + for (final CodeClass clazz : classes) { + clazz.source = source; + } + } + } + } + + private void parseClass(String name, InputStream in) throws IOException { + if (isZip(name)) { + final JarInputStream jar = new JarInputStream(in); + ZipEntry entry; + while ((entry = jar.getNextEntry()) != null) { + try { + if (!entry.isDirectory()) { + parseClass(entry.getName(), jar); + } + } finally { + jar.closeEntry(); + } + } + } else { + addClass(classParser.parse(in, model)); + } + } + + private void addClass(CodeClass clazz) { + final String name = clazz.getSourceFile(); + List classes = classesBySource.get(name); + if (classes == null) { + classes = new ArrayList<>(); + classesBySource.put(name, classes); + } + classes.add(clazz); + } + + private boolean isZip(String name) { + return name.endsWith(".jar") || name.endsWith(".zip") || name.endsWith(".war") || name.endsWith(".ear"); + } + + private boolean isClass(String name) { + return name.endsWith(".class"); + } + +} diff --git a/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFile.java b/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFile.java new file mode 100755 index 0000000..fe3ac91 --- /dev/null +++ b/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFile.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2015 Stefan Niederhauser (nidin@gmx.ch) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package guru.nidi.codeassert.model; + +import java.util.ArrayList; +import java.util.List; + +public class SourceFile { + final String name; + final List classes = new ArrayList<>(); + final int codeLines; + final int commentLines; + final int emptyLines; + final int totalLines; + + SourceFile(String name, int codeLines, int commentLines, int emptyLines, int totalLines) { + this.name = name; + this.codeLines = codeLines; + this.commentLines = commentLines; + this.emptyLines = emptyLines; + this.totalLines = totalLines; + } + + public String getName() { + return name; + } + + public List getClasses() { + return classes; + } + + public int getCodeLines() { + return codeLines; + } + + public int getCommentLines() { + return commentLines; + } + + public int getEmptyLines() { + return emptyLines; + } + + public int getTotalLines() { + return totalLines; + } +} diff --git a/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFileParser.java b/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFileParser.java index e016991..65d7fd8 100755 --- a/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFileParser.java +++ b/code-assert/src/main/java/guru/nidi/codeassert/model/SourceFileParser.java @@ -45,35 +45,32 @@ enum State { int emptyLines; int totalLines; - static CodeClass parse(CodeClass clazz, File file, Charset charset) throws IOException { + static SourceFile parse(File file, Charset charset) throws IOException { try (final InputStream in = new FileInputStream(file)) { final Language language = Language.byFilename(file.getName()); if (language == null) { LOG.info("Unknown source file type {}. Ignoring it", file); - } else { - parse(clazz, language, in, charset); + return null; } - return clazz; + return parse(language, file.getName(), in, charset); } } - static CodeClass parse(CodeClass clazz, Language language, InputStream is, Charset charset) throws IOException { + static SourceFile parse(Language language, String name, InputStream is, Charset charset) throws IOException { try (final Reader in = new InputStreamReader(is, charset)) { - return parse(clazz, language, in); + return parse(language, name, in); } } - static CodeClass parse(CodeClass clazz, Language language, Reader reader) throws IOException { + static SourceFile parse(Language language, String name, Reader reader) throws IOException { try (final BufferedReader in = new BufferedReader(reader)) { final SourceFileParser parser = parser(language); if (parser == null) { LOG.info("No parser for language {}. Ignoring it", language); - } else { - parser.parse(in); - new CodeClassBuilder(clazz) - .addSourceSizes(parser.codeLines, parser.commentLines, parser.emptyLines, parser.totalLines); + return null; } - return clazz; + parser.parse(in); + return new SourceFile(name, parser.codeLines, parser.commentLines, parser.emptyLines, parser.totalLines); } } diff --git a/code-assert/src/test/java/guru/nidi/codeassert/checkstyle/CheckstyleTest.java b/code-assert/src/test/java/guru/nidi/codeassert/checkstyle/CheckstyleTest.java index 9136fc8..5dc0481 100644 --- a/code-assert/src/test/java/guru/nidi/codeassert/checkstyle/CheckstyleTest.java +++ b/code-assert/src/test/java/guru/nidi/codeassert/checkstyle/CheckstyleTest.java @@ -75,7 +75,7 @@ void google() { + line(WARNING, "one.top.level.class", TEST, "model/ExampleConcreteClass", 79, "Top-level class ExamplePackageClass has to reside in its own source file.") + line(WARNING, "overload.methods.declaration", MAIN, "config/BaseCollector", 53, "Overload methods should not be split. Previous overloaded method located at line '47'.") + line(WARNING, "overload.methods.declaration", MAIN, "config/BaseCollector", 64, "Overload methods should not be split. Previous overloaded method located at line '51'.") - + line(WARNING, "overload.methods.declaration", MAIN, "model/SourceFileParser", 91, "Overload methods should not be split. Previous overloaded method located at line '66'.") + + line(WARNING, "overload.methods.declaration", MAIN, "model/SourceFileParser", 88, "Overload methods should not be split. Previous overloaded method located at line '65'.") + line(WARNING, "tag.continuation.indent", MAIN, "dependency/DependencyMap", 105, "Line continuation have incorrect indentation level, expected level should be 4."), analyzer.analyze(), hasNoCheckstyleIssues()); } diff --git a/code-assert/src/test/java/guru/nidi/codeassert/dependency/DependencyRulesTest.java b/code-assert/src/test/java/guru/nidi/codeassert/dependency/DependencyRulesTest.java index ddfbbd4..8ecc0dd 100644 --- a/code-assert/src/test/java/guru/nidi/codeassert/dependency/DependencyRulesTest.java +++ b/code-assert/src/test/java/guru/nidi/codeassert/dependency/DependencyRulesTest.java @@ -16,8 +16,7 @@ package guru.nidi.codeassert.dependency; import guru.nidi.codeassert.config.*; -import guru.nidi.codeassert.model.Model; -import guru.nidi.codeassert.model.Scope; +import guru.nidi.codeassert.model.*; import org.hamcrest.Matcher; import org.hamcrest.StringDescription; import org.junit.jupiter.api.BeforeEach; @@ -53,7 +52,7 @@ public class DependencyRulesTest { @BeforeEach void analyze() { - model = Model.from(AnalyzerConfig.maven().mainAndTest("guru/nidi/codeassert/dependency").getClasses()); + model = new ModelBuilder().files(AnalyzerConfig.maven().mainAndTest("guru/nidi/codeassert/dependency").getClasses()).build(); } @Test @@ -416,7 +415,7 @@ public void defineRules() { assertEquals(new DependencyMap() .with(0, dep("CycleTest"), set(), ca("junit.CodeAssertMatchers")), result3.denied); - assertEquals(71, result.undefined.size()); + assertEquals(72, result.undefined.size()); } private static String ca(String s) { diff --git a/code-assert/src/test/java/guru/nidi/codeassert/detekt/DetektAnalyzerTest.java b/code-assert/src/test/java/guru/nidi/codeassert/detekt/DetektAnalyzerTest.java index b8b8fcd..138f00a 100644 --- a/code-assert/src/test/java/guru/nidi/codeassert/detekt/DetektAnalyzerTest.java +++ b/code-assert/src/test/java/guru/nidi/codeassert/detekt/DetektAnalyzerTest.java @@ -41,7 +41,7 @@ void analyze() { .analyze(); assertMatcher("" + line(Defect, "exceptions", "TooGenericExceptionCatched", "Linker", 45, "Thrown exception is too generic. Prefer throwing project specific exceptions to handle error cases.") - + line(Style, "style", "NewLineAtEndOfFile", "Linker", 59, "Checks whether files end with a line separator.") + + line(Style, "style", "NewLineAtEndOfFile", "Linker", 61, "Checks whether files end with a line separator.") + line(Style, "style", "WildcardImport", "Linker", 19, "Wildcard imports should be replaced with imports using fully qualified class names. Wildcard imports can lead to naming conflicts. A library update can introduce naming clashes with your classes which results in compilation errors."), result, hasNoDetektIssues()); } diff --git a/code-assert/src/test/java/guru/nidi/codeassert/model/AnalyzerTest.java b/code-assert/src/test/java/guru/nidi/codeassert/model/AnalyzerTest.java index b2af643..95134e6 100644 --- a/code-assert/src/test/java/guru/nidi/codeassert/model/AnalyzerTest.java +++ b/code-assert/src/test/java/guru/nidi/codeassert/model/AnalyzerTest.java @@ -23,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; class AnalyzerTest { - final Model model = Model.from(AnalyzerConfig.maven().mainAndTest("guru/nidi/codeassert/model").getClasses()); + final Model model = new ModelBuilder().files(AnalyzerConfig.maven().mainAndTest("guru/nidi/codeassert/model").getClasses()).build(); @Test void packages() throws IOException { @@ -32,6 +32,6 @@ void packages() throws IOException { @Test void classes() throws IOException { - assertEquals(129, model.getClasses().size()); + assertEquals(135, model.getClasses().size()); } } diff --git a/code-assert/src/test/java/guru/nidi/codeassert/model/ModelTest.java b/code-assert/src/test/java/guru/nidi/codeassert/model/ModelTest.java new file mode 100644 index 0000000..8185140 --- /dev/null +++ b/code-assert/src/test/java/guru/nidi/codeassert/model/ModelTest.java @@ -0,0 +1,28 @@ +package guru.nidi.codeassert.model; + +import guru.nidi.codeassert.config.AnalyzerConfig; +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.*; + +public class ModelTest { + @Test + void simple() { + final AnalyzerConfig config = AnalyzerConfig.maven().mainAndTest(); + final Model model = new ModelBuilder().files(config.getClasses()).files(config.getSources()).build(); + assertEquals(82, model.getPackages().size()); + assertEquals(441, model.getClasses().size()); + assertEquals(169, model.getSources().size()); + for (final CodeClass clazz : model.getClasses()) { + System.out.println(clazz); + if (clazz.getPackage().getName().startsWith("guru.nidi")) { + assertTrue(clazz.analyzed); + assertNotNull(clazz.getSource()); + } else { + assertFalse(clazz.analyzed); + assertNull(clazz.getSource()); + } + } + } + +} diff --git a/code-assert/src/test/java/guru/nidi/codeassert/model/SourceFileParserTest.java b/code-assert/src/test/java/guru/nidi/codeassert/model/SourceFileParserTest.java index 9d95588..9dfd92d 100644 --- a/code-assert/src/test/java/guru/nidi/codeassert/model/SourceFileParserTest.java +++ b/code-assert/src/test/java/guru/nidi/codeassert/model/SourceFileParserTest.java @@ -49,7 +49,7 @@ void stringAndComment() throws IOException { @Test void multilineStrings() throws IOException { - assertLines(4, 3, 0, 6, JAVA, "line\n\"\"\"\"\n// line comment\n/*comment*/\n\"\"\"\" //line comment\nlast"); + assertLines(4, 3, 0, 6, JAVA, "line\n\"\"\"\"\n// line comment\n/*comment*/\n\"\"\"\" //line comment\nlast"); assertLines(6, 1, 0, 6, KOTLIN, "line\n\"\"\"\"\n// line comment\n/*comment*/\n\"\"\" //line comment\nlast"); } @@ -60,12 +60,11 @@ void nestedComments() throws IOException { } private void assertLines(int code, int comment, int empty, int total, Language language, String source) throws IOException { - final CodeClass clazz = new CodeClass("test", null); - SourceFileParser.parse(clazz, language, new StringReader(source)); - assertEquals(code, clazz.codeLines); - assertEquals(comment, clazz.commentLines); - assertEquals(empty, clazz.emptyLines); - assertEquals(total, clazz.totalLines); + final SourceFile sf = SourceFileParser.parse(language, "dummy", new StringReader(source)); + assertEquals(code, sf.codeLines); + assertEquals(comment, sf.commentLines); + assertEquals(empty, sf.emptyLines); + assertEquals(total, sf.totalLines); } } diff --git a/code-assert/src/test/java/guru/nidi/codeassert/pmd/PmdTest.java b/code-assert/src/test/java/guru/nidi/codeassert/pmd/PmdTest.java index 20b6286..72c42cd 100644 --- a/code-assert/src/test/java/guru/nidi/codeassert/pmd/PmdTest.java +++ b/code-assert/src/test/java/guru/nidi/codeassert/pmd/PmdTest.java @@ -66,7 +66,7 @@ void pmdIgnore() { + pmd(HIGH, "ClassWithOnlyPrivateConstructorsShouldBeFinal", TEST, "Bugs2", "A class which only has private constructors should be final") + pmd(MEDIUM, "AssignmentInOperand", MAIN, "jacoco/JacocoAnalyzer", "Avoid assignments in operands") + pmd(MEDIUM, "AssignmentInOperand", MAIN, "ktlint/KtlintAnalyzer", "Avoid assignments in operands") - + pmd(MEDIUM, "AssignmentInOperand", MAIN, "model/Model", "Avoid assignments in operands") + + pmd(MEDIUM, "AssignmentInOperand", MAIN, "model/ModelBuilder", "Avoid assignments in operands") + pmd(MEDIUM, "AssignmentInOperand", MAIN, "model/SourceFileParser", "Avoid assignments in operands") + pmd(MEDIUM, "AvoidDuplicateLiterals", MAIN, "pmd/PmdRulesets", "The String literal \"minimum\" appears 5 times in this file; the first occurrence is on line 115") + pmd(MEDIUM, "AvoidDuplicateLiterals", MAIN, "pmd/PmdRulesets", "The String literal \"CommentRequired\" appears 6 times in this file; the first occurrence is on line 154") diff --git a/code-assert/src/test/kotlin/guru/nidi/codeassert/Linker.kt b/code-assert/src/test/kotlin/guru/nidi/codeassert/Linker.kt index d2a819f..ed10971 100644 --- a/code-assert/src/test/kotlin/guru/nidi/codeassert/Linker.kt +++ b/code-assert/src/test/kotlin/guru/nidi/codeassert/Linker.kt @@ -56,4 +56,6 @@ object Linker { """@$trim$rest""" } } -} \ No newline at end of file +} + +fun nop() = 0 \ No newline at end of file