From bffea4bb3bd7ecd8997c0dcc2fa96528926fc417 Mon Sep 17 00:00:00 2001 From: Jan van Nunen Date: Fri, 9 Jan 2015 15:20:25 +0100 Subject: [PATCH] Added support for Scala to CPD. The Scala tokenizer was extracted from the Sonar Scala plugin. (https://github.com/SonarCommunity/sonar-scala) I copied the code because the Sonar Scala plugin has a lot of unwanted dependencies. I tried to keep the changes to the Scala Sonar plugin as minimal as possible. To tokenize the source files the official Scala compiler is used. --- pmd-scala/pom.xml | 92 +++ .../sourceforge/pmd/cpd/ScalaLanguage.java | 19 + .../pmd/lang/scala/ScalaLanguageModule.java | 25 + .../plugins/scala/cpd/ScalaTokenizer.java | 55 ++ .../sonar/plugins/scala/language/Comment.java | 119 ++++ .../plugins/scala/language/CommentType.java | 34 + .../sonar/plugins/scala/util/StringUtils.java | 51 ++ .../services/net.sourceforge.pmd.cpd.Language | 1 + .../net.sourceforge.pmd.lang.Language | 1 + .../plugins/scala/compiler/Compiler.scala | 33 + .../sonar/plugins/scala/compiler/Lexer.scala | 128 ++++ .../sonar/plugins/scala/compiler/Parser.scala | 61 ++ .../sonar/plugins/scala/compiler/Token.scala | 27 + .../plugins/scala/language/CodeDetector.scala | 66 ++ pmd-scala/src/site/markdown/index.md | 3 + pmd-scala/src/site/site.xml | 12 + .../sourceforge/pmd/LanguageVersionTest.java | 27 + .../pmd/cpd/ScalaTokenizerTest.java | 64 ++ .../plugins/scala/cpd/sample-LiftActor.scala | 621 ++++++++++++++++++ pom.xml | 1 + 20 files changed, 1440 insertions(+) create mode 100644 pmd-scala/pom.xml create mode 100644 pmd-scala/src/main/java/net/sourceforge/pmd/cpd/ScalaLanguage.java create mode 100644 pmd-scala/src/main/java/net/sourceforge/pmd/lang/scala/ScalaLanguageModule.java create mode 100644 pmd-scala/src/main/java/org/sonar/plugins/scala/cpd/ScalaTokenizer.java create mode 100644 pmd-scala/src/main/java/org/sonar/plugins/scala/language/Comment.java create mode 100644 pmd-scala/src/main/java/org/sonar/plugins/scala/language/CommentType.java create mode 100644 pmd-scala/src/main/java/org/sonar/plugins/scala/util/StringUtils.java create mode 100644 pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language create mode 100644 pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language create mode 100644 pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Compiler.scala create mode 100644 pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Lexer.scala create mode 100644 pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Parser.scala create mode 100644 pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Token.scala create mode 100644 pmd-scala/src/main/scala/org/sonar/plugins/scala/language/CodeDetector.scala create mode 100644 pmd-scala/src/site/markdown/index.md create mode 100644 pmd-scala/src/site/site.xml create mode 100644 pmd-scala/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java create mode 100644 pmd-scala/src/test/java/net/sourceforge/pmd/cpd/ScalaTokenizerTest.java create mode 100644 pmd-scala/src/test/resources/org/sonar/plugins/scala/cpd/sample-LiftActor.scala diff --git a/pmd-scala/pom.xml b/pmd-scala/pom.xml new file mode 100644 index 0000000000..f76ca3696e --- /dev/null +++ b/pmd-scala/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + pmd-scala + PMD Scala + + + net.sourceforge.pmd + pmd + 5.2.4-SNAPSHOT + + + + 2.10.4 + + + + + + + net.alchim31.maven + scala-maven-plugin + 3.2.0 + + + + + + + maven-resources-plugin + + false + + ${*} + + + + + + net.alchim31.maven + scala-maven-plugin + + + scala-compile-first + process-resources + + add-source + compile + + + + + + + org.apache.maven.plugins + maven-site-plugin + + ${project.build.directory}/generated-xdocs + + + + + + + + + org.scala-lang + scala-library + ${scala.version} + + + org.scala-lang + scala-compiler + ${scala.version} + + + net.sourceforge.pmd + pmd-core + + + + junit + junit + test + + + net.sourceforge.pmd + pmd-test + test + + + diff --git a/pmd-scala/src/main/java/net/sourceforge/pmd/cpd/ScalaLanguage.java b/pmd-scala/src/main/java/net/sourceforge/pmd/cpd/ScalaLanguage.java new file mode 100644 index 0000000000..2f34086344 --- /dev/null +++ b/pmd-scala/src/main/java/net/sourceforge/pmd/cpd/ScalaLanguage.java @@ -0,0 +1,19 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package net.sourceforge.pmd.cpd; + +import org.sonar.plugins.scala.cpd.ScalaTokenizer; + +/** + * Language implementation for Scala + */ +public class ScalaLanguage extends AbstractLanguage { + + /** + * Creates a new Scala Language instance. + */ + public ScalaLanguage() { + super("Scala", "scala", new ScalaTokenizer(), ".scala"); + } +} diff --git a/pmd-scala/src/main/java/net/sourceforge/pmd/lang/scala/ScalaLanguageModule.java b/pmd-scala/src/main/java/net/sourceforge/pmd/lang/scala/ScalaLanguageModule.java new file mode 100644 index 0000000000..29e1af0beb --- /dev/null +++ b/pmd-scala/src/main/java/net/sourceforge/pmd/lang/scala/ScalaLanguageModule.java @@ -0,0 +1,25 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package net.sourceforge.pmd.lang.scala; + +import net.sourceforge.pmd.lang.BaseLanguageModule; + +/** + * Language Module for Scala + */ +public class ScalaLanguageModule extends BaseLanguageModule { + + /** The name. */ + public static final String NAME = "Scala"; + /** The terse name. */ + public static final String TERSE_NAME = "scala"; + + /** + * Create a new instance of Scala Language Module. + */ + public ScalaLanguageModule() { + super(NAME, null, TERSE_NAME, null, "scala"); + addVersion("", null, true); + } +} diff --git a/pmd-scala/src/main/java/org/sonar/plugins/scala/cpd/ScalaTokenizer.java b/pmd-scala/src/main/java/org/sonar/plugins/scala/cpd/ScalaTokenizer.java new file mode 100644 index 0000000000..c18bfe305d --- /dev/null +++ b/pmd-scala/src/main/java/org/sonar/plugins/scala/cpd/ScalaTokenizer.java @@ -0,0 +1,55 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.cpd; + +import java.util.List; + +import net.sourceforge.pmd.cpd.SourceCode; +import net.sourceforge.pmd.cpd.TokenEntry; +import net.sourceforge.pmd.cpd.Tokenizer; +import net.sourceforge.pmd.cpd.Tokens; + +import org.sonar.plugins.scala.compiler.Lexer; +import org.sonar.plugins.scala.compiler.Token; + +/** + * Scala tokenizer for PMD CPD. + * + * @since 0.1 + */ +public final class ScalaTokenizer implements Tokenizer { + + public void tokenize(SourceCode source, Tokens cpdTokens) { + String filename = source.getFileName(); + + Lexer lexer = new Lexer(); + List tokens = lexer.getTokensOfFile(filename); + for (Token token : tokens) { + String tokenVal = + token.tokenVal() != null ? token.tokenVal() : Integer.toString(token.tokenType()); + + TokenEntry cpdToken = new TokenEntry(tokenVal, filename, token.line()); + cpdTokens.add(cpdToken); + } + + cpdTokens.add(TokenEntry.getEOF()); + } + +} diff --git a/pmd-scala/src/main/java/org/sonar/plugins/scala/language/Comment.java b/pmd-scala/src/main/java/org/sonar/plugins/scala/language/Comment.java new file mode 100644 index 0000000000..bfbb992847 --- /dev/null +++ b/pmd-scala/src/main/java/org/sonar/plugins/scala/language/Comment.java @@ -0,0 +1,119 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language; + +import java.io.IOException; +import java.util.List; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.sonar.plugins.scala.util.StringUtils; + +/** + * This class implements a Scala comment and the computation + * of several base metrics for a comment. + * + * @author Felix Müller + * @since 0.1 + */ +public class Comment { + + private final CommentType type; + private final List lines; + + public Comment(String content, CommentType type) throws IOException { + lines = StringUtils.convertStringToListOfLines(content); + this.type = type; + } + + public int getNumberOfLines() { + return lines.size() - getNumberOfBlankLines() - getNumberOfCommentedOutLinesOfCode(); + } + + public int getNumberOfBlankLines() { + int numberOfBlankLines = 0; + for (String comment : lines) { + boolean isBlank = true; + + for (int i = 0; isBlank && i < comment.length(); i++) { + char character = comment.charAt(i); + if (!Character.isWhitespace(character) && character != '*' && character != '/') { + isBlank = false; + } + } + + if (isBlank) { + numberOfBlankLines++; + } + } + return numberOfBlankLines; + } + + public int getNumberOfCommentedOutLinesOfCode() { + if (isDocComment()) { + return 0; + } + + int numberOfCommentedOutLinesOfCode = 0; + for (String line : lines) { + String strippedLine = org.apache.commons.lang3.StringUtils.strip(line, " /*"); + if (CodeDetector.hasDetectedCode(strippedLine)) { + numberOfCommentedOutLinesOfCode++; + } + } + return numberOfCommentedOutLinesOfCode; + } + + public boolean isDocComment() { + return type == CommentType.DOC; + } + + public boolean isHeaderComment() { + return type == CommentType.HEADER; + } + + @Override + public int hashCode() { + return new HashCodeBuilder().append(type).append(lines).toHashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Comment)) { + return false; + } + + Comment other = (Comment) obj; + return new EqualsBuilder().append(type, other.type).append(lines, other.lines).isEquals(); + } + + @Override + public String toString() { + final String firstLine = lines.isEmpty() ? "" : lines.get(0); + final String lastLine = lines.isEmpty() ? "" : lines.get(lines.size() - 1); + return new ToStringBuilder(this).append("type", type) + .append("firstLine", firstLine) + .append("lastLine", lastLine) + .append("numberOfLines", getNumberOfLines()) + .append("numberOfCommentedOutLinesOfCode", getNumberOfCommentedOutLinesOfCode()) + .toString(); + } +} \ No newline at end of file diff --git a/pmd-scala/src/main/java/org/sonar/plugins/scala/language/CommentType.java b/pmd-scala/src/main/java/org/sonar/plugins/scala/language/CommentType.java new file mode 100644 index 0000000000..bf0b19db4c --- /dev/null +++ b/pmd-scala/src/main/java/org/sonar/plugins/scala/language/CommentType.java @@ -0,0 +1,34 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language; + +/** + * This enum is a helper to distinguish between the + * different types of comments in Sonar. + * + * @author Felix Müller + * @since 0.1 + */ +public enum CommentType { + + NORMAL, + DOC, + HEADER; +} \ No newline at end of file diff --git a/pmd-scala/src/main/java/org/sonar/plugins/scala/util/StringUtils.java b/pmd-scala/src/main/java/org/sonar/plugins/scala/util/StringUtils.java new file mode 100644 index 0000000000..4ea6ec8830 --- /dev/null +++ b/pmd-scala/src/main/java/org/sonar/plugins/scala/util/StringUtils.java @@ -0,0 +1,51 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.io.IOUtils; + +public final class StringUtils { + private StringUtils() { + // to prevent instantiation + } + + public static List convertStringToListOfLines(String string) throws IOException { + final List lines = new ArrayList(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new StringReader(string)); + String line = null; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } catch (IOException ioe) { + throw ioe; + } finally { + IOUtils.closeQuietly(reader); + } + return lines; + } +} \ No newline at end of file diff --git a/pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language b/pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language new file mode 100644 index 0000000000..d1e82718f5 --- /dev/null +++ b/pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language @@ -0,0 +1 @@ +net.sourceforge.pmd.cpd.ScalaLanguage diff --git a/pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language b/pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language new file mode 100644 index 0000000000..966058107d --- /dev/null +++ b/pmd-scala/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language @@ -0,0 +1 @@ +net.sourceforge.pmd.lang.scala.ScalaLanguageModule diff --git a/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Compiler.scala b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Compiler.scala new file mode 100644 index 0000000000..c0b4fd371f --- /dev/null +++ b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Compiler.scala @@ -0,0 +1,33 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import scala.tools.nsc.{Settings, Global} + +/** + * This is a wrapper for the Scala compiler. It is used to access + * the compiler in a more convenient way. + * + * @author Felix Müller + * @since 0.1 + */ +object Compiler extends Global(new Settings()) { + override def forScaladoc = true +} diff --git a/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Lexer.scala b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Lexer.scala new file mode 100644 index 0000000000..e6a58f1b18 --- /dev/null +++ b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Lexer.scala @@ -0,0 +1,128 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import collection.mutable.ListBuffer + +import org.sonar.plugins.scala.language.{Comment, CommentType} +import scala.reflect.io.AbstractFile +import scala.reflect.internal.util.BatchSourceFile + +/** + * This class is a wrapper for accessing the lexer of the Scala compiler + * from Java in a more convenient way. + * + * @author Felix Müller + * @since 0.1 + */ +class Lexer { + + import scala.collection.JavaConversions._ + import Compiler._ + + def getTokens(code: String): java.util.List[Token] = { + val unit = new CompilationUnit(new BatchSourceFile("", code.toCharArray)) + tokenize(unit) + } + + def getTokensOfFile(path: String): java.util.List[Token] = { + val unit = new CompilationUnit(new BatchSourceFile(AbstractFile.getFile(path))) + tokenize(unit) + } + + private def tokenize(unit: CompilationUnit): java.util.List[Token] = { + val scanner = new syntaxAnalyzer.UnitScanner(unit) + val tokens = ListBuffer[Token]() + + scanner.init() + while (scanner.token != scala.tools.nsc.ast.parser.Tokens.EOF) { + val tokenVal = + if (scala.tools.nsc.ast.parser.Tokens.isIdentifier(scanner.token)) scanner.name.toString() else null + val linenr = scanner.parensAnalyzer.line(scanner.offset) + 1 + + tokens += Token(scanner.token, linenr, tokenVal) + scanner.nextToken() + } + tokens + } + + def getComments(code: String): java.util.List[Comment] = { + val unit = new CompilationUnit(new BatchSourceFile("", code.toCharArray)) + tokenizeComments(unit) + } + + def getCommentsOfFile(path: String): java.util.List[Comment] = { + val unit = new CompilationUnit(new BatchSourceFile(AbstractFile.getFile(path))) + tokenizeComments(unit) + } + + private def tokenizeComments(unit: CompilationUnit): java.util.List[Comment] = { + val comments = ListBuffer[Comment]() + val scanner = new syntaxAnalyzer.UnitScanner(unit) { + + private var lastDocCommentRange: Option[Range] = None + + private var foundToken = false + + override def nextToken() { + super.nextToken() + foundToken = token != 0 + } + + override def foundComment(value: String, start: Int, end: Int) = { + super.foundComment(value, start, end) + + def isHeaderComment(value: String) = { + !foundToken && comments.isEmpty && value.trim().startsWith("/*") + } + + lastDocCommentRange match { + + case Some(r: Range) => { + if (r.start != start || r.end != end) { + comments += new Comment(value, CommentType.NORMAL) + } + } + + case None => { + if (isHeaderComment(value)) { + comments += new Comment(value, CommentType.HEADER) + } else { + comments += new Comment(value, CommentType.NORMAL) + } + } + } + } + + override def foundDocComment(value: String, start: Int, end: Int) = { + super.foundDocComment(value, start, end) + comments += new Comment(value, CommentType.DOC) + lastDocCommentRange = Some(Range(start, end)) + } + } + + scanner.init() + while (scanner.token != scala.tools.nsc.ast.parser.Tokens.EOF) { + scanner.nextToken() + } + + comments + } +} \ No newline at end of file diff --git a/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Parser.scala b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Parser.scala new file mode 100644 index 0000000000..935508719a --- /dev/null +++ b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Parser.scala @@ -0,0 +1,61 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +import scala.reflect.internal.util.{ScriptSourceFile, BatchSourceFile} +import scala.reflect.io.AbstractFile + +/** + * This class is a wrapper for accessing the parser of the Scala compiler + * from Java in a more convenient way. + * + * @author Felix Müller + * @since 0.1 + */ +class Parser { + + import Compiler._ + + def parse(code: String): Tree = { + val batchSourceFile = new BatchSourceFile("", code.toCharArray) + parse(batchSourceFile, code.toCharArray) + } + + def parseFile(path: String): Tree = { + val batchSourceFile = new BatchSourceFile(AbstractFile.getFile(path)) + parse(batchSourceFile, batchSourceFile.content.array) + } + + private def parse(batchSourceFile: BatchSourceFile, code: Array[Char]): Tree = { + try { + val parser = new syntaxAnalyzer.SourceFileParser(new ScriptSourceFile(batchSourceFile, code, 0)) + val tree = parser.templateStatSeq(false)._2 + parser.makePackaging(0, parser.atPos(0, 0, 0)(Ident(nme.EMPTY_PACKAGE_NAME)), tree) + } catch { + case _: Throwable => { + val unit = new CompilationUnit(batchSourceFile) + val unitParser = new syntaxAnalyzer.UnitParser(unit) { + override def showSyntaxErrors() {} + } + unitParser.smartParse() + } + } + } +} diff --git a/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Token.scala b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Token.scala new file mode 100644 index 0000000000..977188295c --- /dev/null +++ b/pmd-scala/src/main/scala/org/sonar/plugins/scala/compiler/Token.scala @@ -0,0 +1,27 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.compiler + +/** + * Represent a token. Lines must start at 1. + * + * @since 0.1 + */ +case class Token(tokenType: Int, line: Int, tokenVal: String) diff --git a/pmd-scala/src/main/scala/org/sonar/plugins/scala/language/CodeDetector.scala b/pmd-scala/src/main/scala/org/sonar/plugins/scala/language/CodeDetector.scala new file mode 100644 index 0000000000..f99fae7fed --- /dev/null +++ b/pmd-scala/src/main/scala/org/sonar/plugins/scala/language/CodeDetector.scala @@ -0,0 +1,66 @@ +/* + * Sonar Scala Plugin + * Copyright (C) 2011 - 2014 All contributors + * dev@sonar.codehaus.org + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.plugins.scala.language + +import org.sonar.plugins.scala.compiler.{ Compiler, Parser } + +/** + * This object is a helper object for detecting valid Scala code + * in a given piece of source code. + * + * @author Felix Müller + * @since 0.1 + */ +object CodeDetector { + + import Compiler._ + + private lazy val parser = new Parser() + + def hasDetectedCode(code: String) = { + + def lookingForSyntaxTreesWithCode(tree: Tree) : Boolean = tree match { + + case PackageDef(identifier: RefTree, content) => + if (!identifier.name.equals(nme.EMPTY_PACKAGE_NAME)) { + true + } else { + content.exists(lookingForSyntaxTreesWithCode) + } + + case Apply(function, args) => + args.exists(lookingForSyntaxTreesWithCode) + + case ClassDef(_, _, _, _) + | ModuleDef(_, _, _) + | ValDef(_, _, _, _) + | DefDef(_, _, _, _, _, _) + | Function(_ , _) + | Assign(_, _) + | LabelDef(_, _, _) => + true + + case _ => + false + } + + lookingForSyntaxTreesWithCode(parser.parse(code)) + } +} \ No newline at end of file diff --git a/pmd-scala/src/site/markdown/index.md b/pmd-scala/src/site/markdown/index.md new file mode 100644 index 0000000000..0ea01c0741 --- /dev/null +++ b/pmd-scala/src/site/markdown/index.md @@ -0,0 +1,3 @@ +# PMD Scala + +Only CPD is supported. There are no PMD rules for Scala. diff --git a/pmd-scala/src/site/site.xml b/pmd-scala/src/site/site.xml new file mode 100644 index 0000000000..09f0f1fda0 --- /dev/null +++ b/pmd-scala/src/site/site.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/pmd-scala/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java b/pmd-scala/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java new file mode 100644 index 0000000000..af7a228025 --- /dev/null +++ b/pmd-scala/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java @@ -0,0 +1,27 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package net.sourceforge.pmd; + +import java.util.Arrays; +import java.util.Collection; + +import net.sourceforge.pmd.lang.LanguageRegistry; +import net.sourceforge.pmd.lang.LanguageVersion; +import net.sourceforge.pmd.lang.scala.ScalaLanguageModule; + +import org.junit.runners.Parameterized.Parameters; + +public class LanguageVersionTest extends AbstractLanguageVersionTest { + + public LanguageVersionTest(String name, String terseName, String version, LanguageVersion expected) { + super(name, terseName, version, expected); + } + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { ScalaLanguageModule.NAME, ScalaLanguageModule.TERSE_NAME, "", LanguageRegistry.getLanguage(ScalaLanguageModule.NAME).getDefaultVersion() } + }); + } +} diff --git a/pmd-scala/src/test/java/net/sourceforge/pmd/cpd/ScalaTokenizerTest.java b/pmd-scala/src/test/java/net/sourceforge/pmd/cpd/ScalaTokenizerTest.java new file mode 100644 index 0000000000..44c00d48dd --- /dev/null +++ b/pmd-scala/src/test/java/net/sourceforge/pmd/cpd/ScalaTokenizerTest.java @@ -0,0 +1,64 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package net.sourceforge.pmd.cpd; + +import java.io.File; +import java.io.IOException; + +import net.sourceforge.pmd.testframework.AbstractTokenizerTest; +import net.sourceforge.pmd.testframework.StreamUtil; + +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.sonar.plugins.scala.cpd.ScalaTokenizer; + + +public class ScalaTokenizerTest extends AbstractTokenizerTest { + + private static final String FILENAME = "sample-LiftActor.scala"; + + private File tempFile; + + @Before + @Override + public void buildTokenizer() { + createTempFileOnDisk(); + + this.tokenizer = new ScalaTokenizer(); + this.sourceCode = new SourceCode(new SourceCode.FileCodeLoader(tempFile, "UTF-8")); + } + + private void createTempFileOnDisk() { + try { + this.tempFile = File.createTempFile("scala-tokenizer-test-", ".scala"); + + FileUtils.writeStringToFile(tempFile, getSampleCode(), "UTF-8"); + } catch (IOException e) { + throw new RuntimeException("Unable to create temporary file on disk for Scala tokenizer test", e); + } + } + + @Override + public String getSampleCode() { + return StreamUtil.toString(ScalaTokenizer.class.getResourceAsStream(FILENAME)); + } + + @Test + public void tokenizeTest() throws IOException { + this.expectedTokenCount = 2591; + super.tokenizeTest(); + } + + @After + public void cleanUp() { + FileUtils.deleteQuietly(this.tempFile); + this.tempFile = null; + } + + public static junit.framework.Test suite() { + return new junit.framework.JUnit4TestAdapter(ScalaTokenizerTest.class); + } +} diff --git a/pmd-scala/src/test/resources/org/sonar/plugins/scala/cpd/sample-LiftActor.scala b/pmd-scala/src/test/resources/org/sonar/plugins/scala/cpd/sample-LiftActor.scala new file mode 100644 index 0000000000..142aa96f20 --- /dev/null +++ b/pmd-scala/src/test/resources/org/sonar/plugins/scala/cpd/sample-LiftActor.scala @@ -0,0 +1,621 @@ +/* Example source code copied from the Lift project on GitHub + * https://github.com/lift/framework/blob/master/core/actor/src/main/scala/net/liftweb/actor/LiftActor.scala + * + * Copyright 2009-2011 WorldWide Conferencing, LLC + * + * 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 net.liftweb +package actor + +import common._ + +trait ILAExecute { + def execute(f: () => Unit): Unit + def shutdown(): Unit +} + +/** + * The definition of a scheduler + */ +trait LAScheduler { + /** + * Execute some code on another thread + * + * @param f the function to execute on another thread + */ + def execute(f: () => Unit): Unit +} + +object LAScheduler extends LAScheduler with Loggable { + @volatile + var onSameThread = false + + /** + * Set this variable to the number of threads to allocate in the thread pool + */ + @volatile var threadPoolSize = 16 // issue 194 + + @volatile var maxThreadPoolSize = threadPoolSize * 25 + + /** + * If it's Full, then create an ArrayBlockingQueue, + * otherwise create a LinkedBlockingQueue. Default + * to Full(200000). + */ + @volatile var blockingQueueSize: Box[Int] = Full(200000) + + @volatile + var createExecutor: () => ILAExecute = () => { + new ILAExecute { + import java.util.concurrent._ + + private val es = // Executors.newFixedThreadPool(threadPoolSize) + new ThreadPoolExecutor(threadPoolSize, + maxThreadPoolSize, + 60, + TimeUnit.SECONDS, + blockingQueueSize match { + case Full(x) => + new ArrayBlockingQueue(x) + case _ => new LinkedBlockingQueue + }) + + def execute(f: () => Unit): Unit = + es.execute(new Runnable{def run() { + try { + f() + } catch { + case e: Exception => logger.error("Lift Actor Scheduler", e) + } + }}) + + def shutdown(): Unit = { + es.shutdown() + } + } + } + + @volatile + var exec: ILAExecute = _ + + /** + * Execute some code on another thread + * + * @param f the function to execute on another thread + */ + def execute(f: () => Unit) { + synchronized { + if (exec eq null) { + exec = createExecutor() + } + exec.execute(f) + } + } + + def shutdown() { + synchronized { + if (exec ne null) { + exec.shutdown() + } + + exec = null + } + } +} + +trait SpecializedLiftActor[T] extends SimpleActor[T] { + @volatile private[this] var processing = false + private[this] val baseMailbox: MailboxItem = new SpecialMailbox + @volatile private[this] var msgList: List[T] = Nil + @volatile private[this] var priorityMsgList: List[T] = Nil + @volatile private[this] var startCnt = 0 + + private class MailboxItem(val item: T) { + var next: MailboxItem = _ + var prev: MailboxItem = _ + + /* + def find(f: MailboxItem => Boolean): Box[MailboxItem] = + if (f(this)) Full(this) else next.find(f) + */ + + def remove() { + val newPrev = prev + prev.next = next + next.prev = prev + } + + def insertAfter(newItem: MailboxItem): MailboxItem = { + next.prev = newItem + newItem.prev = this + newItem.next = this.next + next = newItem + newItem + } + + def insertBefore(newItem: MailboxItem): MailboxItem = { + prev.next = newItem + newItem.prev = this.prev + newItem.next = this + prev = newItem + newItem + } + } + + private class SpecialMailbox extends MailboxItem(null.asInstanceOf[T]) { + // override def find(f: MailboxItem => Boolean): Box[MailboxItem] = Empty + next = this + prev = this + } + + private def findMailboxItem(start: MailboxItem, f: MailboxItem => Boolean): Box[MailboxItem] = + start match { + case x: SpecialMailbox => Empty + case x if f(x) => Full(x) + case x => findMailboxItem(x.next, f) + } + + /** + * Send a message to the Actor. This call will always succeed + * and return almost immediately. The message will be processed + * asynchronously. This is a Java-callable alias for !. + */ + def send(msg: T): Unit = this ! msg + + /** + * Send a message to the Actor. This call will always succeed + * and return almost immediately. The message will be processed + * asynchronously. + */ + def !(msg: T): Unit = { + val toDo: () => Unit = baseMailbox.synchronized { + msgList ::= msg + if (!processing) { + if (LAScheduler.onSameThread) { + processing = true + () => processMailbox(true) + } else { + if (startCnt == 0) { + startCnt += 1 + () => LAScheduler.execute(() => processMailbox(false)) + } else + () => {} + } + } + else () => {} + } + toDo() + } + + /** + * This method inserts the message at the head of the mailbox. + * It's protected because this functionality may or may not want + * to be exposed. + */ + protected def insertMsgAtHeadOfQueue_!(msg: T): Unit = { + val toDo: () => Unit = baseMailbox.synchronized { + this.priorityMsgList ::= msg + if (!processing) { + if (LAScheduler.onSameThread) { + processing = true + () => processMailbox(true) + } else { + if (startCnt == 0) { + startCnt += 1 + () => LAScheduler.execute(() => processMailbox(false)) + } else + () => {} + } + } + else () => {} + } + toDo() + } + + private def processMailbox(ignoreProcessing: Boolean) { + around { + proc2(ignoreProcessing) + } + } + + /** + * A list of LoanWrappers that will be executed around the evaluation of mailboxes + */ + protected def aroundLoans: List[CommonLoanWrapper] = Nil + + /** + * You can wrap calls around the evaluation of the mailbox. This allows you to set up + * the environment. + */ + protected def around[R](f: => R): R = aroundLoans match { + case Nil => f + case xs => CommonLoanWrapper(xs)(f) + } + private def proc2(ignoreProcessing: Boolean) { + var clearProcessing = true + baseMailbox.synchronized { + if (!ignoreProcessing && processing) return + processing = true + if (startCnt > 0) startCnt = 0 + } + + val eh = exceptionHandler + + def putListIntoMB(): Unit = { + if (!priorityMsgList.isEmpty) { + priorityMsgList.foldRight(baseMailbox)((msg, mb) => mb.insertAfter(new MailboxItem(msg))) + priorityMsgList = Nil + } + + if (!msgList.isEmpty) { + msgList.foldLeft(baseMailbox)((mb, msg) => mb.insertBefore(new MailboxItem(msg))) + msgList = Nil + } + } + + try { + while (true) { + baseMailbox.synchronized { + putListIntoMB() + } + + var keepOnDoingHighPriory = true + + while (keepOnDoingHighPriory) { + val hiPriPfBox = highPriorityReceive + hiPriPfBox.map{ + hiPriPf => + findMailboxItem(baseMailbox.next, mb => testTranslate(hiPriPf.isDefinedAt)(mb.item)) match { + case Full(mb) => + mb.remove() + try { + execTranslate(hiPriPf)(mb.item) + } catch { + case e: Exception => if (eh.isDefinedAt(e)) eh(e) + } + case _ => + baseMailbox.synchronized { + if (msgList.isEmpty) { + keepOnDoingHighPriory = false + } + else { + putListIntoMB() + } + } + } + }.openOr{keepOnDoingHighPriory = false} + } + + val pf = messageHandler + + findMailboxItem(baseMailbox.next, mb => testTranslate(pf.isDefinedAt)(mb.item)) match { + case Full(mb) => + mb.remove() + try { + execTranslate(pf)(mb.item) + } catch { + case e: Exception => if (eh.isDefinedAt(e)) eh(e) + } + case _ => + baseMailbox.synchronized { + if (msgList.isEmpty) { + processing = false + clearProcessing = false + return + } + else { + putListIntoMB() + } + } + } + } + } catch { + case exception: Throwable => + if (eh.isDefinedAt(exception)) + eh(exception) + + throw exception + } finally { + if (clearProcessing) { + baseMailbox.synchronized { + processing = false + } + } + } + } + + protected def testTranslate(f: T => Boolean)(v: T): Boolean = f(v) + + protected def execTranslate(f: T => Unit)(v: T): Unit = f(v) + + protected def messageHandler: PartialFunction[T, Unit] + + protected def highPriorityReceive: Box[PartialFunction[T, Unit]] = Empty + + protected def exceptionHandler: PartialFunction[Throwable, Unit] = { + case e => ActorLogger.error("Actor threw an exception", e) + } +} + +/** + * A SpecializedLiftActor designed for use in unit testing of other components. + * + * Messages sent to an actor extending this interface are not processed, but are instead + * recorded in a List. The intent is that when you are testing some other component (say, a snippet) + * that should send a message to an actor, the test for that snippet should simply test that + * the actor received the message, not what the actor does with that message. If an actor + * implementing this trait is injected into the component you're testing (in place of the + * real actor) you gain the ability to run these kinds of tests. +**/ +class MockSpecializedLiftActor[T] extends SpecializedLiftActor[T] { + private[this] var messagesReceived: List[T] = Nil + + /** + * Send a message to the mock actor, which will be recorded and not processed by the + * message handler. + **/ + override def !(msg: T): Unit = { + messagesReceived.synchronized { + messagesReceived ::= msg + } + } + + // We aren't required to implement a real message handler for the Mock actor + // since the message handler never runs. + override def messageHandler: PartialFunction[T, Unit] = { + case _ => + } + + /** + * Test to see if this actor has received a particular message. + **/ + def hasReceivedMessage_?(msg: T): Boolean = messagesReceived.contains(msg) + + /** + * Returns the list of messages the mock actor has received. + **/ + def messages: List[T] = messagesReceived + + /** + * Return the number of messages this mock actor has received. + **/ + def messageCount: Int = messagesReceived.size +} + +object ActorLogger extends Logger { +} + +private final case class MsgWithResp(msg: Any, future: LAFuture[Any]) + +trait LiftActor extends SpecializedLiftActor[Any] +with GenericActor[Any] +with ForwardableActor[Any, Any] { + @volatile + private[this] var responseFuture: LAFuture[Any] = null + + + + protected final def forwardMessageTo(msg: Any, forwardTo: TypedActor[Any, Any]) { + if (null ne responseFuture) { + forwardTo match { + case la: LiftActor => la ! MsgWithResp(msg, responseFuture) + case other => + reply(other !? msg) + } + } else forwardTo ! msg + } + + /** + * Send a message to the Actor and get an LAFuture + * that will contain the reply (if any) from the message. + * This method calls !< and is here for Java compatibility. + */ + def sendAndGetFuture(msg: Any): LAFuture[Any] = this !< msg + + /** + * Send a message to the Actor and get an LAFuture + * that will contain the reply (if any) from the message + */ + def !<(msg: Any): LAFuture[Any] = { + val future = new LAFuture[Any] + this ! MsgWithResp(msg, future) + future + } + + /** + * Send a message to the Actor and wait for + * the actor to process the message and reply. + * This method is the Java callable version of !?. + */ + def sendAndGetReply(msg: Any): Any = this !? msg + + /** + * Send a message to the Actor and wait for + * the actor to process the message and reply. + */ + def !?(msg: Any): Any = { + val future = new LAFuture[Any] + this ! MsgWithResp(msg, future) + future.get + } + + + /** + * Send a message to the Actor and wait for + * up to timeout milliseconds for + * the actor to process the message and reply. + * This method is the Java callable version of !?. + */ + def sendAndGetReply(timeout: Long, msg: Any): Any = this.!?(timeout, msg) + + /** + * Send a message to the Actor and wait for + * up to timeout milliseconds for + * the actor to process the message and reply. + */ + def !?(timeout: Long, message: Any): Box[Any] = + this !! (message, timeout) + + + /** + * Send a message to the Actor and wait for + * up to timeout milliseconds for + * the actor to process the message and reply. + */ + def !!(msg: Any, timeout: Long): Box[Any] = { + val future = new LAFuture[Any] + this ! MsgWithResp(msg, future) + future.get(timeout) + } + + /** + * Send a message to the Actor and wait for + * the actor to process the message and reply. + */ + def !!(msg: Any): Box[Any] = { + val future = new LAFuture[Any] + this ! MsgWithResp(msg, future) + Full(future.get) + } + + override protected def testTranslate(f: Any => Boolean)(v: Any) = v match { + case MsgWithResp(msg, _) => f(msg) + case v => f(v) + } + + override protected def execTranslate(f: Any => Unit)(v: Any) = v match { + case MsgWithResp(msg, future) => + responseFuture = future + try { + f(msg) + } finally { + responseFuture = null + } + case v => f(v) + } + + /** + * The Actor should call this method with a reply + * to the message + */ + protected def reply(v: Any) { + if (null ne responseFuture) { + responseFuture.satisfy(v) + } + } +} + +/** + * A MockLiftActor for use in testing other compnents that talk to actors. + * + * Much like MockSpecializedLiftActor, this class is intended to be injected into other + * components, such as snippets, during testing. Whereas these components would normally + * talk to a real actor that would process their message, this mock actor simply + * records them and exposes methods the unit test can use to investigate what messages + * have been received by the actor. +**/ +class MockLiftActor extends MockSpecializedLiftActor[Any] with LiftActor + +import java.lang.reflect._ + +object LiftActorJ { + private var methods: Map[Class[_], DispatchVendor] = Map() + + def calculateHandler(what: LiftActorJ): PartialFunction[Any, Unit] = + synchronized { + val clz = what.getClass + methods.get(clz) match { + case Some(pf) => pf.vend(what) + case _ => { + val pf = buildPF(clz) + methods += clz -> pf + pf.vend(what) + } + } + } + + private def getBaseClasses(clz: Class[_]): List[Class[_]] = clz match { + case null => Nil + case clz => clz :: getBaseClasses(clz.getSuperclass) + } + + private def receiver(in: Method): Boolean = { + in.getParameterTypes().length == 1 && + (in.getAnnotation(classOf[JavaActorBase.Receive]) != null) + } + + private def buildPF(clz: Class[_]): DispatchVendor = { + val methods = getBaseClasses(clz). + flatMap(_.getDeclaredMethods.toList.filter(receiver)) + + val clzMap: Map[Class[_], Method] = + Map(methods.map{m => + m.setAccessible(true) // access private and protected methods + m.getParameterTypes().apply(0) -> m} :_*) + + new DispatchVendor(clzMap) + } +} + +private final class DispatchVendor(map: Map[Class[_], Method]) { + private val baseMap: Map[Class[_], Option[Method]] = + Map(map.map{case (k,v) => (k, Some(v))}.toList :_*) + + def vend(actor: LiftActorJ): PartialFunction[Any, Unit] = + new PartialFunction[Any, Unit] { + var theMap: Map[Class[_], Option[Method]] = baseMap + + def findClass(clz: Class[_]): Option[Method] = + theMap.find(_._1.isAssignableFrom(clz)).flatMap(_._2) + + def isDefinedAt(v: Any): Boolean = { + val clz = v.asInstanceOf[Object].getClass + theMap.get(clz) match { + case Some(Some(_)) => true + case None => { + val answer = findClass(clz) + theMap += clz -> answer + answer.isDefined + } + case _ => false + } + } + + def apply(v: Any): Unit = { + val o: Object = v.asInstanceOf[Object] + val meth = theMap(o.getClass).get + meth.invoke(actor, o) match { + case null => + case x => actor.internalReply(x) + } + } + } +} + +/** + * Java versions of Actors should subclass this method. + * Methods decorated with the @Receive annotation + * will receive messages of that type. + */ +class LiftActorJ extends JavaActorBase with LiftActor { + protected lazy val _messageHandler: PartialFunction[Any, Unit] = + calculateJavaMessageHandler + + protected def calculateJavaMessageHandler = LiftActorJ.calculateHandler(this) + + protected def messageHandler = _messageHandler + + private[actor] def internalReply(v: Any) = reply(v) +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 3380601831..cd5667b109 100644 --- a/pom.xml +++ b/pom.xml @@ -867,6 +867,7 @@ pmd-php pmd-plsql pmd-ruby + pmd-scala pmd-test pmd-vm pmd-xml