diff --git a/pmd-cs/pom.xml b/pmd-cs/pom.xml
new file mode 100644
index 0000000000..3679f4c65d
--- /dev/null
+++ b/pmd-cs/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+ pmd-cs
+ PMD C#
+
+
+ net.sourceforge.pmd
+ pmd
+ 5.5.0-SNAPSHOT
+
+
+
+ ${basedir}/../pmd-core
+
+
+
+
+
+ maven-resources-plugin
+
+ false
+
+ ${*}
+
+
+
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+
+
+
+ net.sourceforge.pmd
+ pmd-test
+ test
+
+
+
diff --git a/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsLanguage.java b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsLanguage.java
new file mode 100644
index 0000000000..f6a1bd145f
--- /dev/null
+++ b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsLanguage.java
@@ -0,0 +1,26 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+package net.sourceforge.pmd.cpd;
+
+import java.util.Properties;
+
+/**
+ * Language implementation for C#
+ */
+public class CsLanguage extends AbstractLanguage {
+
+ public CsLanguage() {
+ this(System.getProperties());
+ }
+
+ public CsLanguage(Properties properties) {
+ super("C#", "cs", new CsTokenizer(), ".cs");
+ setProperties(properties);
+ }
+
+ public final void setProperties(Properties properties) {
+ CsTokenizer tokenizer = (CsTokenizer)getTokenizer();
+ tokenizer.setProperties(properties);
+ }
+}
diff --git a/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java
new file mode 100644
index 0000000000..c9dcce4a4e
--- /dev/null
+++ b/pmd-cs/src/main/java/net/sourceforge/pmd/cpd/CsTokenizer.java
@@ -0,0 +1,306 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+package net.sourceforge.pmd.cpd;
+
+import java.io.BufferedReader;
+import java.io.CharArrayReader;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.PushbackReader;
+import java.util.Properties;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+
+/**
+ * This class does a best-guess try-anything tokenization.
+ *
+ * @author jheintz
+ */
+public class CsTokenizer implements Tokenizer {
+
+ private boolean ignoreUsings = false;
+
+ public void setProperties(Properties properties) {
+ if (properties.containsKey(IGNORE_USINGS)) {
+ ignoreUsings = Boolean.parseBoolean(properties.getProperty(IGNORE_USINGS, "false"));
+ }
+ }
+
+ @Override
+ public void tokenize(SourceCode sourceCode, Tokens tokenEntries) {
+ Tokenizer tokenizer =
+ new Tokenizer(sourceCode.getCodeBuffer().toString());
+ Token token = tokenizer.getNextToken();
+
+ while (!token.equals(Token.EOF)) {
+ Token lookAhead = tokenizer.getNextToken();
+
+ // Ignore using directives
+ // Only using directives should be ignored, because these are used to import namespaces
+ //
+ // Using directive: 'using System.Math;'
+ // Using statement: 'using (Font font1 = new Font(..)) { .. }'
+ if (ignoreUsings &&
+ "using".equals(token.image) &&
+ !"(".equals(lookAhead.image)
+ ) {
+ // We replace the 'using' token by a random token, because it should not be part of
+ // any duplication block. When we omit it from the token stream, there is a change that
+ // we get a duplication block that starts before the 'using' directives and ends afterwards.
+ String randomTokenText =
+ RandomStringUtils.randomAlphanumeric(20);
+
+ token = new Token(randomTokenText, token.lineNumber);
+ //Skip all other tokens of the using directive to prevent a partial matching
+ while (!";".equals(lookAhead.image) && !lookAhead.equals(Token.EOF)) {
+ lookAhead = tokenizer.getNextToken();
+ }
+ }
+ if (!";".equals(token.image)) {
+ tokenEntries.add(new TokenEntry(token.image, sourceCode.getFileName(), token.lineNumber));
+ }
+ token = lookAhead;
+ }
+ tokenEntries.add(TokenEntry.getEOF());
+ IOUtils.closeQuietly(tokenizer);
+ }
+
+ public void setIgnoreUsings(boolean ignoreUsings) {
+ this.ignoreUsings = ignoreUsings;
+ }
+
+
+ private static class Tokenizer implements Closeable {
+ private boolean endOfFile;
+ private int line;
+ private final PushbackReader reader;
+
+ public Tokenizer(String sourceCode) {
+ endOfFile = false;
+ line = 1;
+ reader = new PushbackReader(new BufferedReader(new CharArrayReader(sourceCode.toCharArray())));
+ }
+
+ public Token getNextToken() {
+ if (endOfFile) {
+ return Token.EOF;
+ }
+
+ try {
+ int ic = reader.read();
+ char c;
+ StringBuilder b;
+ while (ic != -1) {
+ c = (char) ic;
+ switch (c) {
+ // new line
+ case '\n':
+ line++;
+ ic = reader.read();
+ break;
+
+ // white space
+ case ' ':
+ case '\t':
+ case '\r':
+ ic = reader.read();
+ break;
+
+ case ';':
+ return new Token(";", line);
+
+ // < << <= <<= > >> >= >>=
+ case '<':
+ case '>':
+ ic = reader.read();
+ if (ic == '=') {
+ return new Token(c + "=", line);
+ } else if (ic == c) {
+ ic = reader.read();
+ if (ic == '=') {
+ return new Token(c + c + "=", line);
+ } else {
+ reader.unread(ic);
+ return new Token(String.valueOf(c) + c, line);
+ }
+ } else {
+ reader.unread(ic);
+ return new Token(String.valueOf(c), line);
+ }
+
+ // = == & &= && | |= || + += ++ - -= --
+ case '=':
+ case '&':
+ case '|':
+ case '+':
+ case '-':
+ ic = reader.read();
+ if (ic == '=' || ic == c) {
+ return new Token(c + String.valueOf((char) ic), line);
+ } else {
+ reader.unread(ic);
+ return new Token(String.valueOf(c), line);
+ }
+
+ // ! != * *= % %= ^ ^= ~ ~=
+ case '!':
+ case '*':
+ case '%':
+ case '^':
+ case '~':
+ ic = reader.read();
+ if (ic == '=') {
+ return new Token(c + "=", line);
+ } else {
+ reader.unread(ic);
+ return new Token(String.valueOf(c), line);
+ }
+
+ // strings & chars
+ case '"':
+ case '\'':
+ int beginLine = line;
+ b = new StringBuilder();
+ b.append(c);
+ while ((ic = reader.read()) != c) {
+ if (ic == -1) {
+ break;
+ }
+ b.append((char) ic);
+ if (ic == '\\') {
+ int next = reader.read();
+ if (next != -1) {
+ b.append((char) next);
+
+ if (next == '\n') {
+ line++;
+ }
+ }
+ } else if (ic == '\n') {
+ line++;
+ }
+ }
+ if (ic != -1) {
+ b.append((char) ic);
+ }
+ return new Token(b.toString(), beginLine);
+
+ // / /= /*...*/ //...
+ case '/':
+ switch (c = (char) (ic = reader.read())) {
+ case '*':
+ //int beginLine = line;
+ int state = 1;
+ b = new StringBuilder();
+ b.append("/*");
+
+ while ((ic = reader.read()) != -1) {
+ c = (char) ic;
+ b.append(c);
+
+ if (c == '\n') {
+ line++;
+ }
+
+ if (state == 1) {
+ if (c == '*') {
+ state = 2;
+ }
+ } else {
+ if (c == '/') {
+ ic = reader.read();
+ break;
+ } else if (c != '*') {
+ state = 1;
+ }
+ }
+ }
+ // ignore the /* comment
+ // tokenEntries.add(new TokenEntry(b.toString(),
+ // sourceCode.getFileName(), beginLine));
+ break;
+
+ case '/':
+ b = new StringBuilder();
+ b.append("//");
+ while ((ic = reader.read()) != '\n') {
+ if (ic == -1) {
+ break;
+ }
+ b.append((char) ic);
+ }
+ // ignore the // comment
+ // tokenEntries.add(new TokenEntry(b.toString(),
+ // sourceCode.getFileName(), line));
+ break;
+
+ case '=':
+ return new Token("/=", line);
+
+ default:
+ reader.unread(ic);
+ return new Token("/", line);
+ }
+ break;
+
+ default:
+ // [a-zA-Z_][a-zA-Z_0-9]*
+ if (Character.isJavaIdentifierStart(c)) {
+ b = new StringBuilder();
+ do {
+ b.append(c);
+ c = (char) (ic = reader.read());
+ } while (Character.isJavaIdentifierPart(c));
+ reader.unread(ic);
+ return new Token(b.toString(), line);
+ }
+ // numbers
+ else if (Character.isDigit(c) || c == '.') {
+ b = new StringBuilder();
+ do {
+ b.append(c);
+ if (c == 'e' || c == 'E') {
+ c = (char) (ic = reader.read());
+ if ("1234567890-".indexOf(c) == -1) {
+ break;
+ }
+ b.append(c);
+ }
+ c = (char) (ic = reader.read());
+ } while ("1234567890.iIlLfFdDsSuUeExX".indexOf(c) != -1);
+ reader.unread(ic);
+ return new Token(b.toString(), line);
+ }
+ // anything else
+ else {
+ return new Token(String.valueOf(c), line);
+ }
+ }
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ endOfFile = true;
+ return Token.EOF;
+ }
+
+ @Override
+ public void close() throws IOException {
+ reader.close();
+ }
+ }
+
+ private static class Token {
+ public static final Token EOF = new Token("EOF", -1);
+
+ public final String image;
+ public final int lineNumber;
+
+ public Token(String image, int lineNumber) {
+ this.image = image;
+ this.lineNumber = lineNumber;
+ }
+ }
+}
diff --git a/pmd-cs/src/main/java/net/sourceforge/pmd/lang/cs/CsLanguageModule.java b/pmd-cs/src/main/java/net/sourceforge/pmd/lang/cs/CsLanguageModule.java
new file mode 100644
index 0000000000..c24e2ffdbd
--- /dev/null
+++ b/pmd-cs/src/main/java/net/sourceforge/pmd/lang/cs/CsLanguageModule.java
@@ -0,0 +1,25 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+package net.sourceforge.pmd.lang.cs;
+
+import net.sourceforge.pmd.lang.BaseLanguageModule;
+
+/**
+ * Language Module for C#
+ */
+public class CsLanguageModule extends BaseLanguageModule {
+
+ /** The name. */
+ public static final String NAME = "C#";
+ /** The terse name. */
+ public static final String TERSE_NAME = "cs";
+
+ /**
+ * Create a new instance of C# Language Module.
+ */
+ public CsLanguageModule() {
+ super(NAME, null, TERSE_NAME, null, "cs");
+ addVersion("", null, true);
+ }
+}
diff --git a/pmd-cs/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language b/pmd-cs/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language
new file mode 100644
index 0000000000..76459b4741
--- /dev/null
+++ b/pmd-cs/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language
@@ -0,0 +1 @@
+net.sourceforge.pmd.cpd.CsLanguage
diff --git a/pmd-cs/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language b/pmd-cs/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language
new file mode 100644
index 0000000000..1b979f896f
--- /dev/null
+++ b/pmd-cs/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language
@@ -0,0 +1 @@
+net.sourceforge.pmd.lang.cs.CsLanguageModule
diff --git a/pmd-cs/src/site/markdown/index.md b/pmd-cs/src/site/markdown/index.md
new file mode 100644
index 0000000000..4a4bca4837
--- /dev/null
+++ b/pmd-cs/src/site/markdown/index.md
@@ -0,0 +1,3 @@
+# PMD C##
+
+Only CPD is supported. There are no PMD rules for C#.
diff --git a/pmd-cs/src/site/site.xml b/pmd-cs/src/site/site.xml
new file mode 100644
index 0000000000..e7be177e12
--- /dev/null
+++ b/pmd-cs/src/site/site.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java b/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java
new file mode 100644
index 0000000000..9c1a9a9014
--- /dev/null
+++ b/pmd-cs/src/test/java/net/sourceforge/pmd/cpd/CsTokenizerTest.java
@@ -0,0 +1,165 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+
+package net.sourceforge.pmd.cpd;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class CsTokenizerTest {
+
+ private CsTokenizer tokenizer;
+
+ private Tokens tokens;
+
+ @Before
+ public void init() {
+ tokenizer = new CsTokenizer();
+ tokens = new Tokens();
+ TokenEntry.clearImages();
+ }
+
+ @Test
+ public void testSimpleClass() {
+ tokenizer.tokenize(toSourceCode("class Foo {}"), tokens);
+ assertEquals(5, tokens.size());
+ }
+
+ @Test
+ public void testSimpleClassDuplicatedTokens() {
+ tokenizer.tokenize(toSourceCode("class Foo { class Foo { } }"), tokens);
+ assertEquals(9, tokens.size());
+ List tokenList = tokens.getTokens();
+ assertEquals(tokenList.get(0).getIdentifier(), tokenList.get(3).getIdentifier());
+ assertEquals(tokenList.get(1).getIdentifier(), tokenList.get(4).getIdentifier());
+ assertEquals(tokenList.get(2).getIdentifier(), tokenList.get(5).getIdentifier());
+ assertEquals(tokenList.get(6).getIdentifier(), tokenList.get(7).getIdentifier());
+ }
+
+ @Test
+ public void testSimpleClassMethodMultipleLines() {
+ tokenizer.tokenize(toSourceCode(
+ "class Foo {\n"
+ + " public String foo(int a) {\n"
+ + " int i = a;\n"
+ + " return \"x\" + a;\n"
+ + " }\n"
+ + "}"), tokens);
+ assertEquals(22, tokens.size());
+ List tokenList = tokens.getTokens();
+ assertEquals(1, tokenList.get(0).getBeginLine());
+ assertEquals(2, tokenList.get(4).getBeginLine());
+ assertEquals(3, tokenList.get(11).getBeginLine());
+ }
+
+ @Test
+ public void testStrings() {
+ tokenizer.tokenize(toSourceCode("String s =\"aaa \\\"b\\n\";"), tokens);
+ assertEquals(5, tokens.size());
+ }
+
+ @Test
+ public void testOpenString() {
+ tokenizer.tokenize(toSourceCode("String s =\"aaa \\\"b\\"), tokens);
+ assertEquals(5, tokens.size());
+ }
+
+
+ @Test
+ public void testCommentsIgnored1() {
+ tokenizer.tokenize(toSourceCode("class Foo { /* class * ** X */ }"), tokens);
+ assertEquals(5, tokens.size());
+ }
+
+ @Test
+ public void testCommentsIgnored2() {
+ tokenizer.tokenize(toSourceCode("class Foo { // class X /* aaa */ \n }"), tokens);
+ assertEquals(5, tokens.size());
+ }
+
+ @Test
+ public void testCommentsIgnored3() {
+ tokenizer.tokenize(toSourceCode("class Foo { /// class X /* aaa */ \n }"), tokens);
+ assertEquals(5, tokens.size());
+ }
+
+ @Test
+ public void testMoreTokens() {
+ tokenizer.tokenize(toSourceCode(
+ "class Foo {\n"
+ + " void bar() {\n"
+ + " int a = 1 >> 2; \n"
+ + " a += 1; \n"
+ + " a++; \n"
+ + " a /= 3e2; \n"
+ + " float f = -3.1; \n"
+ + " f *= 2; \n"
+ + " bool b = ! (f == 2.0 || f >= 1.0 && f <= 2.0) \n"
+ + " }\n"
+ + "}"
+ ), tokens);
+ assertEquals(50, tokens.size());
+ }
+
+ @Test
+ public void testLineNumberAfterMultilineComment() {
+ tokenizer.tokenize(toSourceCode(
+ "/* This is a multiline comment \n"
+ + " * \n"
+ + " * Lorem ipsum dolor sit amet, \n"
+ + " * consectetur adipiscing elit \n"
+ + " */\n"
+ + "\n"
+ + "class Foo {\n"
+ + "\n"
+ + "}"
+ ), tokens);
+ assertEquals(5, tokens.size());
+ assertEquals(7, tokens.getTokens().get(0).getBeginLine());
+ }
+
+ @Test
+ public void testLineNumberAfterMultilineString() {
+ tokenizer.tokenize(toSourceCode(
+ "class Foo {\n"
+ + " void bar() {\n"
+ + " String query = \n"
+ + " @\"SELECT foo, bar\n"
+ + " FROM table \n"
+ + " WHERE id = 42\"; \n"
+ + " }\n"
+ + "}"
+ ), tokens);
+ assertEquals(16, tokens.size());
+ assertEquals(8, tokens.getTokens().get(14).getBeginLine());
+ }
+
+ @Test
+ public void testIgnoreUsingDirectives() {
+ tokenizer.setIgnoreUsings(true);
+ tokenizer.tokenize(toSourceCode("using System.Text;\n"), tokens);
+ assertNotEquals("using", tokens.getTokens().get(0).toString());
+ assertEquals(2, tokens.size());
+ }
+
+ @Test
+ public void testUsingStatementsAreNotIgnored() {
+ tokenizer.setIgnoreUsings(true);
+ tokenizer.tokenize(toSourceCode(
+ "using (Font font1 = new Font(\"Arial\", 10.0f)) {\n"
+ + " byte charset = font1.GdiCharSet;\n"
+ + "}\n"
+ ), tokens);
+ assertEquals("using", tokens.getTokens().get(0).toString());
+ }
+
+ private SourceCode toSourceCode(String source) {
+ return new SourceCode(new SourceCode.StringCodeLoader(source));
+ }
+}
diff --git a/pmd-fortran/pom.xml b/pmd-fortran/pom.xml
new file mode 100644
index 0000000000..88368bd27e
--- /dev/null
+++ b/pmd-fortran/pom.xml
@@ -0,0 +1,42 @@
+
+
+ 4.0.0
+ pmd-fortran
+ PMD Fortran
+
+
+ net.sourceforge.pmd
+ pmd
+ 5.5.0-SNAPSHOT
+
+
+
+ ${basedir}/../pmd-core
+
+
+
+
+
+ maven-resources-plugin
+
+ false
+
+ ${*}
+
+
+
+
+
+
+
+ net.sourceforge.pmd
+ pmd-core
+
+
+
+ net.sourceforge.pmd
+ pmd-test
+ test
+
+
+
diff --git a/pmd-fortran/src/main/java/net/sourceforge/pmd/cpd/FortranLanguage.java b/pmd-fortran/src/main/java/net/sourceforge/pmd/cpd/FortranLanguage.java
new file mode 100644
index 0000000000..6e6faa0342
--- /dev/null
+++ b/pmd-fortran/src/main/java/net/sourceforge/pmd/cpd/FortranLanguage.java
@@ -0,0 +1,17 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+package net.sourceforge.pmd.cpd;
+
+/**
+ * Language implementation for Fortran
+ * @author Romain PELISSE belaran@gmail.com
+ */
+public class FortranLanguage extends AbstractLanguage {
+ /**
+ * Create a Fotran Language instance.
+ */
+ public FortranLanguage() {
+ super("Fortran", "fortran", new FortranTokenizer(), ".for", ".f", ".f66", ".f77", ".f90");
+ }
+}
diff --git a/pmd-fortran/src/main/java/net/sourceforge/pmd/cpd/FortranTokenizer.java b/pmd-fortran/src/main/java/net/sourceforge/pmd/cpd/FortranTokenizer.java
new file mode 100644
index 0000000000..8cae493bc2
--- /dev/null
+++ b/pmd-fortran/src/main/java/net/sourceforge/pmd/cpd/FortranTokenizer.java
@@ -0,0 +1,38 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+package net.sourceforge.pmd.cpd;
+
+import java.util.ArrayList;
+
+/**
+ * Tokenizer implementation for Fortran
+ *
+ * @author Romain PELISSE - romain.pelisse@atosorigin.com
+ */
+public class FortranTokenizer extends AbstractTokenizer implements Tokenizer {
+
+ /**
+ * Creates a new instance of {@link FortranTokenizer}.
+ */
+ public FortranTokenizer() {
+ this.spanMultipleLinesString = false; // No such thing in Fortran !
+ // setting markers for "string" in Fortran
+ this.stringToken = new ArrayList();
+ this.stringToken.add("\'");
+ // setting markers for 'ignorable character' in Fortran
+ this.ignorableCharacter = new ArrayList();
+ this.ignorableCharacter.add("(");
+ this.ignorableCharacter.add(")");
+ this.ignorableCharacter.add(",");
+
+ // setting markers for 'ignorable string' in Fortran
+ this.ignorableStmt = new ArrayList();
+ this.ignorableStmt.add("do");
+ this.ignorableStmt.add("while");
+ this.ignorableStmt.add("end");
+ this.ignorableStmt.add("if");
+ // Fortran comment start with an !
+ this.oneLineCommentChar = '!';
+ }
+}
diff --git a/pmd-fortran/src/main/java/net/sourceforge/pmd/lang/fortran/FortranLanguageModule.java b/pmd-fortran/src/main/java/net/sourceforge/pmd/lang/fortran/FortranLanguageModule.java
new file mode 100644
index 0000000000..b41d56c418
--- /dev/null
+++ b/pmd-fortran/src/main/java/net/sourceforge/pmd/lang/fortran/FortranLanguageModule.java
@@ -0,0 +1,26 @@
+/**
+ * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
+ */
+package net.sourceforge.pmd.lang.fortran;
+
+import net.sourceforge.pmd.lang.BaseLanguageModule;
+
+/**
+ * Implementation for the Fortran Language Module
+ */
+public class FortranLanguageModule extends BaseLanguageModule {
+
+ /** The name */
+ public static final String NAME = "Fortran";
+ /** The terse name */
+ public static final String TERSE_NAME = "fortran";
+
+ /**
+ * Creates a new instance of {@link FortranLanguageModule}
+ */
+ public FortranLanguageModule() {
+ super(NAME, null, TERSE_NAME, null, "for", "f", "f66", "f77", "f90");
+ addVersion("", null, true);
+ }
+
+}
diff --git a/pmd-fortran/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language b/pmd-fortran/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language
new file mode 100644
index 0000000000..f78952ce23
--- /dev/null
+++ b/pmd-fortran/src/main/resources/META-INF/services/net.sourceforge.pmd.cpd.Language
@@ -0,0 +1 @@
+net.sourceforge.pmd.cpd.FortranLanguage
diff --git a/pmd-fortran/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language b/pmd-fortran/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language
new file mode 100644
index 0000000000..bf4133fb47
--- /dev/null
+++ b/pmd-fortran/src/main/resources/META-INF/services/net.sourceforge.pmd.lang.Language
@@ -0,0 +1 @@
+net.sourceforge.pmd.lang.fortran.FortranLanguageModule
diff --git a/pmd-fortran/src/site/markdown/index.md b/pmd-fortran/src/site/markdown/index.md
new file mode 100644
index 0000000000..808bc4fa7b
--- /dev/null
+++ b/pmd-fortran/src/site/markdown/index.md
@@ -0,0 +1,3 @@
+# PMD Fortran
+
+Only CPD is supported. There are no PMD rules for Fortran.
diff --git a/pmd-fortran/src/site/site.xml b/pmd-fortran/src/site/site.xml
new file mode 100644
index 0000000000..156ddcb354
--- /dev/null
+++ b/pmd-fortran/src/site/site.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/pmd-fortran/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java b/pmd-fortran/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java
new file mode 100644
index 0000000000..041ba4887e
--- /dev/null
+++ b/pmd-fortran/src/test/java/net/sourceforge/pmd/LanguageVersionTest.java
@@ -0,0 +1,28 @@
+/**
+ * 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.AbstractLanguageVersionTest;
+import net.sourceforge.pmd.lang.LanguageRegistry;
+import net.sourceforge.pmd.lang.LanguageVersion;
+import net.sourceforge.pmd.lang.fortran.FortranLanguageModule;
+
+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