linter
diff --git a/build.xml b/build.xml
index 82af803..0e57eec 100644
--- a/build.xml
+++ b/build.xml
@@ -71,7 +71,7 @@
<target name="swf" depends="sdk, javadoc, tests" description="Builds SWF compiler, builds Royale Javadoc, and runs SWF compiler tests."/>
- <target name="main" depends="swf, jx, oem, debugger, formatter, anttasks, royaleunit.anttasks" description="Builds Royale SWF compiler, then Royale JS Transpiler"/>
+ <target name="main" depends="swf, jx, oem, debugger, formatter, linter, anttasks, royaleunit.anttasks" description="Builds Royale SWF compiler, then Royale JS Transpiler"/>
<target name="jx" depends="compiler.jx, compiler.jx.tests" description="Builds Royale JS Transpiler" />
@@ -104,6 +104,10 @@
<target name="formatter" description="Builds formatter JAR">
<ant dir="formatter" target="main"/>
</target>
+
+ <target name="linter" description="Builds linter JAR">
+ <ant dir="linter" target="main"/>
+ </target>
<target name="compiler.jx" description="Builds Royale JS Transpiler.">
<ant dir="compiler-jx" target="main"/>
@@ -147,6 +151,7 @@
<ant dir="royaleunit-ant-tasks" target="clean"/>
<ant dir="debugger" target="clean"/>
<ant dir="formatter" target="clean"/>
+ <ant dir="linter" target="clean"/>
</target>
<target name="wipe" description="Wipes out everything that didn't come from Git.">
@@ -177,6 +182,7 @@
<ant dir="royaleunit-ant-tasks" target="wipe"/>
<ant dir="debugger" target="clean"/>
<ant dir="formatter" target="clean"/>
+ <ant dir="linter" target="clean"/>
<delete dir="${basedir}/out" failonerror="false" includeEmptyDirs="true"/>
<delete dir="${basedir}/temp" failonerror="false" includeEmptyDirs="true"/>
<!-- remove legacy folders if they exist -->
@@ -297,6 +303,9 @@
<!-- formatter -->
<antcall target="stage-formatter"/>
+ <!-- linter -->
+ <antcall target="stage-linter"/>
+
<!-- flex-compiler-oem and fdb -->
<antcall target="stage-fb-integration"/>
@@ -470,6 +479,24 @@
</copy>
</target>
+ <target name="stage-linter">
+ <copy todir="${staging-dir}/linter" includeEmptyDirs="false">
+ <fileset dir="${basedir}/linter">
+ <include name="**"/>
+ <exclude name=".classpath" />
+ <exclude name=".project" />
+ <exclude name=".settings/**" />
+ <exclude name="in/**"/>
+ <exclude name="lib/**"/>
+ <exclude name="target/classes/**"/>
+ <exclude name="target/test-classes/**"/>
+ <exclude name="target/junit-temp/**"/>
+ <exclude name="target/junit-reports/**"/>
+ <exclude name="**/unittest.properties" />
+ </fileset>
+ </copy>
+ </target>
+
<target name="stage-fb-integration">
<copy todir="${staging-dir}/debugger" includeEmptyDirs="false">
<fileset dir="${basedir}/debugger">
diff --git a/compiler/src/assembly/scripts/aslint b/compiler/src/assembly/scripts/aslint
new file mode 100644
index 0000000..b23185a
--- /dev/null
+++ b/compiler/src/assembly/scripts/aslint
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+################################################################################
+##
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements. See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You 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.
+##
+################################################################################
+
+#
+# fasc shell script to launch compiler-asc.jar on OSX, Unix, or Cygwin.
+# In Windows Command Prompt, use fasc.bat instead.
+#
+
+if [ "x${ROYALE_COMPILER_HOME}" = "x" ]
+then
+ SCRIPT_HOME=`dirname "$0"`
+ ROYALE_COMPILER_HOME=${SCRIPT_HOME}/..
+else
+ echo Using Royale codebase: $ROYALE_COMPILER_HOME
+fi
+
+case `uname` in
+ CYGWIN*)
+ OS="Windows"
+ ;;
+ *)
+ OS=Unix
+esac
+
+D32=''
+
+if [ $OS = "Windows" ]; then
+
+ ROYALE_COMPILER_HOME=`cygpath -m $ROYALE_COMPILER_HOME`
+
+elif [ $OS = "Unix" ]; then
+
+ check64="`java -version 2>&1 | grep -i 64-Bit`"
+ isOSX="`uname | grep -i Darwin`"
+ javaVersion="`java -version 2>&1 | awk -F '[ ".]+' 'NR==1 {print $3 "." $4}'`"
+
+ if [ "$isOSX" != "" -a "$HOSTTYPE" = "x86_64" -a "$check64" != "" -a "$javaVersion" = "1.6" ]; then
+ D32='-d32'
+ fi
+fi
+
+VMARGS="-Xmx384m -Dsun.io.useCanonCaches=false "
+
+java $VMARGS $D32 $SETUP_SH_VMARGS -jar "${ROYALE_COMPILER_HOME}/lib/linter.jar" "$@"
diff --git a/compiler/src/assembly/scripts/aslint.bat b/compiler/src/assembly/scripts/aslint.bat
new file mode 100644
index 0000000..5bc7dc9
--- /dev/null
+++ b/compiler/src/assembly/scripts/aslint.bat
@@ -0,0 +1,26 @@
+@echo off
+
+rem
+rem Licensed to the Apache Software Foundation (ASF) under one or more
+rem contributor license agreements. See the NOTICE file distributed with
+rem this work for additional information regarding copyright ownership.
+rem The ASF licenses this file to You under the Apache License, Version 2.0
+rem (the "License"); you may not use this file except in compliance with
+rem the License. You may obtain a copy of the License at
+rem
+rem http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem Unless required by applicable law or agreed to in writing, software
+rem distributed under the License is distributed on an "AS IS" BASIS,
+rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem See the License for the specific language governing permissions and
+rem limitations under the License.
+rem
+
+rem
+rem fasc.bat script to launch compiler-asc.jar in Windows Command Prompt.
+rem On OSX, Unix, or Cygwin, use the fasc shell script instead.
+rem
+
+@java -Dsun.io.useCanonCaches=false -Dapplication.home="%~dp0.." -Xms32m -Xmx512m -jar "%~dp0..\lib\linter.jar" %*
+
diff --git a/linter/build.xml b/linter/build.xml
new file mode 100644
index 0000000..40ad821
--- /dev/null
+++ b/linter/build.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" ?>
+
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+
+-->
+
+<project name="linter" default="main" basedir=".">
+
+ <!--
+
+ PROPERTIES
+
+ -->
+
+ <!-- The 'linter' property is the absolute path, with forward slashes, -->
+ <!-- to the 'linter' directory that contains this file. -->
+ <!-- All input paths are expressed as absolute paths starting with ${linter}. -->
+ <pathconvert property="linter" dirsep="/">
+ <path location="${basedir}"/>
+ </pathconvert>
+
+ <!-- The 'compiler' property is the absolute path, with forward slashes, -->
+ <!-- to the 'compiler' directory that is a sibling of the parent folder of this file. -->
+ <!-- All input paths are expressed as absolute paths starting with ${compiler}. -->
+ <pathconvert property="compiler" dirsep="/">
+ <path location="${basedir}/../compiler"/>
+ </pathconvert>
+
+ <property name="sdk" value="${compiler}/lib"/>
+
+ <!-- Properties can be overridden locally by loading a local.properties file -->
+ <!-- Java 8 users probably need javadoc.params=-Xdoclint:none -->
+ <property file="${linter}/local.properties"/>
+ <property file="${basedir}/../env.properties"/>
+
+ <property name="src.depend" value="true"/>
+
+ <!-- Options for <javac> tasks -->
+ <property name="javac.debug" value="true"/>
+ <property name="javac.deprecation" value="false"/>
+ <property name="linter.javac.src" value="1.8"/>
+
+ <!-- JAR manifest entries -->
+ <property name="manifest.sealed" value="false"/>
+ <property name="manifest.Implementation-Title" value="Apache Royale ActionScript Linter"/>
+ <property name="manifest.Implementation-Version" value="${release.version}"/>
+ <property name="manifest.Implementation-Vendor" value="Apache Software Foundation"/>
+
+ <!-- label is set by CruiseControl script based on P4 label incrementer -->
+ <condition property="build.number" value="${env.BUILD_NUMBER}">
+ <isset property="env.BUILD_NUMBER"/>
+ </condition>
+
+ <!--
+
+ linter
+
+ -->
+
+ <target name="compile">
+ <mkdir dir="${linter}/target/classes"/>
+ <javac debug="${javac.debug}" deprecation="${javac.deprecation}" destdir="${linter}/target/classes" includeAntRuntime="true" includes="**/*.java"
+ source="${linter.javac.src}" target="${linter.javac.src}">
+ <compilerarg value="-Xlint:all,-path,-fallthrough"/>
+ <src path="${linter}/src/main/java"/>
+ <classpath>
+ <fileset dir="${linter}/../compiler/lib" includes="compiler.jar"/>
+ </classpath>
+ </javac>
+ </target>
+
+ <target name="prebuild">
+ <mkdir dir="${linter}/target/classes"/>
+ <mkdir dir="${linter}/target/classes/META-INF"/>
+ </target>
+
+ <target name="jar" depends="prebuild,compile">
+ <mkdir dir="${sdk}"/>
+ <copy file="${basedir}/../LICENSE.base" tofile="${linter}/target/classes/META-INF/LICENSE"/>
+ <copy file="${basedir}/../NOTICE.base" tofile="${linter}/target/classes/META-INF/NOTICE"/>
+ <jar file="${sdk}/linter.jar" basedir="${linter}/target/classes" includes="**/*.properties,org/apache/**/*,META-INF/**/*">
+ <include name="META-INF/LICENSE"/>
+ <include name="META-INF/NOTICE"/>
+ <manifest>
+ <attribute name="Sealed" value="${manifest.sealed}"/>
+ <attribute name="Implementation-Title" value="${manifest.Implementation-Title}"/>
+ <attribute name="Implementation-Version" value="${manifest.Implementation-Version}.${build.number}"/>
+ <attribute name="Implementation-Vendor" value="${manifest.Implementation-Vendor}"/>
+ <attribute name="Main-Class" value="org.apache.royale.linter.LINTER"/>
+ <attribute name="Class-Path" value="compiler.jar"/>
+ </manifest>
+ </jar>
+ </target>
+
+ <target name="main" depends="jar">
+ <!-- <antcall target="test"/> -->
+ <antcall target="jar-test"/>
+ </target>
+
+ <target name="test">
+ <ant dir="src/test"/>
+ </target>
+
+ <target name="jar-test">
+ <echo>using linter.jar from ${sdk}</echo>
+ <java jar="${sdk}/linter.jar" fork="true" resultproperty="linter.result"/>
+ <fail message="Starting Failed">
+ <condition>
+ <not>
+ <equals arg1="${linter.result}" arg2="1"/>
+ </not>
+ </condition>
+ </fail>
+ </target>
+
+ <!--
+
+ CLEANUP
+
+ -->
+
+ <target name="clean" description="clean">
+ <delete dir="${linter}/target/classes"/>
+ </target>
+
+ <target name="wipe" depends="clean" description="Wipes everything that didn't come from Git.">
+ <delete dir="${linter}/target"/>
+ </target>
+
+ </project>
diff --git a/linter/pom.xml b/linter/pom.xml
new file mode 100644
index 0000000..9cada6c
--- /dev/null
+++ b/linter/pom.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+
+--><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.royale.compiler</groupId>
+ <artifactId>royale-compiler-parent</artifactId>
+ <version>0.9.10-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>linter</artifactId>
+ <version>0.9.10-SNAPSHOT</version>
+
+ <name>Apache Royale: Compiler: AS3/MXML Code Linter</name>
+ <description>The Apache Royale Compiler AS3/MXML Code Linter</description>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>default-jar</id>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>org.apache.royale.linter.LINTER</mainClass>
+ </manifest>
+ <manifestEntries>
+ <!-- These paths are all defined the way the layout will be in the distribution -->
+ <Class-Path>compiler.jar</Class-Path>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ </execution>
+ <execution>
+ <id>default-test-jar</id>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <includes>
+ <include>**/Test*.java</include>
+ </includes>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.royale.compiler</groupId>
+ <artifactId>compiler</artifactId>
+ <version>0.9.10-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.10</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/linter/src/main/java/org/apache/royale/linter/ASLinter.java b/linter/src/main/java/org/apache/royale/linter/ASLinter.java
new file mode 100644
index 0000000..17f403c
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/ASLinter.java
@@ -0,0 +1,217 @@
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.royale.compiler.internal.parsing.as.ASParser;
+import org.apache.royale.compiler.internal.parsing.as.ASToken;
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.internal.parsing.as.IncludeHandler;
+import org.apache.royale.compiler.internal.parsing.as.RepairingTokenBuffer;
+import org.apache.royale.compiler.internal.parsing.as.StreamingASTokenizer;
+import org.apache.royale.compiler.internal.semantics.PostProcessStep;
+import org.apache.royale.compiler.internal.tree.as.FileNode;
+import org.apache.royale.compiler.internal.workspaces.Workspace;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.problems.UnexpectedExceptionProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.linter.internal.BaseLinter;
+
+public class ASLinter extends BaseLinter {
+ private static final String LINTER_TAG_OFF = "@linter:off";
+ private static final String LINTER_TAG_ON = "@linter:on";
+
+ public ASLinter(LinterSettings settings) {
+ super(settings);
+ }
+
+ public void lint(String filePath, String text, Collection<ICompilerProblem> allProblems) {
+ if (allProblems == null) {
+ allProblems = new ArrayList<ICompilerProblem>();
+ }
+
+ List<ICompilerProblem> fileProblems = new ArrayList<ICompilerProblem>();
+ try {
+ StringReader textReader = new StringReader(text);
+ StreamingASTokenizer tokenizer = null;
+ ASToken[] streamingTokens = null;
+ try {
+ tokenizer = StreamingASTokenizer.createForRepairingASTokenizer(textReader, filePath,
+ IncludeHandler.creatDefaultIncludeHandler());
+ tokenizer.setCollectComments(true);
+ tokenizer.setFollowIncludes(false);
+ streamingTokens = tokenizer.getTokens(textReader);
+ } finally {
+ IOUtils.closeQuietly(textReader);
+ IOUtils.closeQuietly(tokenizer);
+ }
+
+ if (tokenizer.hasTokenizationProblems()) {
+ fileProblems.addAll(tokenizer.getTokenizationProblems());
+ }
+
+ if (!settings.ignoreProblems && hasErrors(fileProblems)) {
+ return;
+ }
+
+ // temporarily remove the comments from the token list because ASParser
+ // doesn't know how to deal with them properly.
+ // we'll add them back at the same locations after the parser is done.
+ List<ASToken> comments = new ArrayList<ASToken>();
+ List<ASToken> streamingTokensList = new ArrayList<ASToken>();
+ for (ASToken token : streamingTokens) {
+ if (token.getType() == ASTokenTypes.HIDDEN_TOKEN_SINGLE_LINE_COMMENT
+ || token.getType() == ASTokenTypes.HIDDEN_TOKEN_MULTI_LINE_COMMENT) {
+ comments.add(token);
+ } else {
+ streamingTokensList.add(token);
+ }
+ }
+
+ Workspace workspace = new Workspace();
+ RepairingTokenBuffer buffer = new RepairingTokenBuffer(streamingTokensList.toArray(new ASToken[0]));
+ ASParser parser = new ASParser(workspace, buffer);
+ FileNode node = new FileNode(workspace);
+ try {
+ parser.parseFile(node, EnumSet.of(PostProcessStep.CALCULATE_OFFSETS));
+ } catch (Exception e) {
+ parser = null;
+ fileProblems.add(new UnexpectedExceptionProblem(e));
+ return;
+ }
+
+ if (tokenizer.hasTokenizationProblems()) {
+ fileProblems.addAll(tokenizer.getTokenizationProblems());
+ }
+
+ if (parser.getSyntaxProblems().size() > 0) {
+ fileProblems.addAll(parser.getSyntaxProblems());
+ }
+
+ if (!settings.ignoreProblems && hasErrors(fileProblems)) {
+ return;
+ }
+
+ List<IASToken> repairedTokensList = new ArrayList<IASToken>(Arrays.asList(buffer.getTokens(true)));
+ // restore the comments that were removed before parsing
+ IASToken nextComment = null;
+ for (int i = 0; i < repairedTokensList.size(); i++) {
+ if (nextComment == null) {
+ if (comments.size() == 0) {
+ // no more comments to add
+ break;
+ }
+ nextComment = comments.get(0);
+ }
+ IASToken currentToken = repairedTokensList.get(i);
+ if (nextComment.getAbsoluteStart() <= currentToken.getAbsoluteStart()) {
+ repairedTokensList.add(i, nextComment);
+ nextComment = null;
+ comments.remove(0);
+ }
+ }
+ // there may be some comments left that didn't appear before any
+ // of the repaired tokens, so add them all at the end
+ repairedTokensList.addAll(comments);
+
+ IASToken[] allTokens = repairedTokensList.toArray(new IASToken[0]);
+ TokenQuery tokenQuery = new TokenQuery(allTokens);
+ visitNode(node, tokenQuery, fileProblems);
+ boolean skipLinting = false;
+ for (LinterRule rule : settings.rules) {
+ Map<Integer, TokenVisitor> tokenHandlers = rule.getTokenVisitors();
+ if (tokenHandlers != null) {
+ for (IASToken token : allTokens) {
+ int tokenType = token.getType();
+ if (tokenType == ASTokenTypes.HIDDEN_TOKEN_SINGLE_LINE_COMMENT
+ || tokenType == ASTokenTypes.HIDDEN_TOKEN_MULTI_LINE_COMMENT) {
+ boolean isMultiline = tokenType == ASTokenTypes.HIDDEN_TOKEN_MULTI_LINE_COMMENT;
+ String tokenText = token.getText();
+ String trimmed = tokenText.substring(2, tokenText.length() - (isMultiline ? 2 : 0)).trim();
+ if (!skipLinting && LINTER_TAG_OFF.equals(trimmed)) {
+ skipLinting = true;
+ } else if (skipLinting && LINTER_TAG_ON.equals(trimmed)) {
+ skipLinting = false;
+ continue;
+ }
+ }
+ if (skipLinting) {
+ continue;
+ }
+ if (tokenHandlers.containsKey(tokenType)) {
+ tokenHandlers.get(tokenType).visit(token, tokenQuery, fileProblems);
+ }
+ }
+ }
+ }
+ } finally {
+ allProblems.addAll(fileProblems);
+ }
+ }
+
+ private void visitNode(IASNode node, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ ASTNodeID nodeID = node.getNodeID();
+ IASToken prevComment = tokenQuery.getPreviousComment(node);
+ while (prevComment != null) {
+ String commentText = null;
+ if (prevComment.getType() == ASTokenTypes.HIDDEN_TOKEN_SINGLE_LINE_COMMENT) {
+ commentText = prevComment.getText().substring(2).trim();
+ } else if (prevComment.getType() == ASTokenTypes.HIDDEN_TOKEN_MULTI_LINE_COMMENT) {
+ commentText = prevComment.getText();
+ commentText = commentText.substring(2, commentText.length() - 2).trim();
+ } else {
+ // not the type of comment that we care about
+ prevComment = tokenQuery.getPreviousComment(prevComment);
+ continue;
+ }
+ if (LINTER_TAG_ON.equals(commentText)) {
+ // linter is on
+ break;
+ }
+ if (LINTER_TAG_OFF.equals(commentText)) {
+ // linter is off
+ return;
+ }
+ prevComment = tokenQuery.getPreviousComment(prevComment);
+ }
+ for (LinterRule rule : settings.rules) {
+ Map<ASTNodeID, NodeVisitor> nodeHandlers = rule.getNodeVisitors();
+ if (nodeHandlers != null && nodeHandlers.containsKey(nodeID)) {
+ nodeHandlers.get(nodeID).visit(node, tokenQuery, problems);
+ }
+ }
+
+ for (int i = 0; i < node.getChildCount(); i++) {
+ IASNode child = node.getChild(i);
+ visitNode(child, tokenQuery, problems);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/LINTER.java b/linter/src/main/java/org/apache/royale/linter/LINTER.java
new file mode 100644
index 0000000..38b49f9
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/LINTER.java
@@ -0,0 +1,448 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.royale.compiler.clients.problems.CompilerProblemCategorizer;
+import org.apache.royale.compiler.clients.problems.ProblemFormatter;
+import org.apache.royale.compiler.clients.problems.ProblemPrinter;
+import org.apache.royale.compiler.clients.problems.ProblemQuery;
+import org.apache.royale.compiler.clients.problems.WorkspaceProblemFormatter;
+import org.apache.royale.compiler.common.VersionInfo;
+import org.apache.royale.compiler.exceptions.ConfigurationException;
+import org.apache.royale.compiler.filespecs.FileSpecification;
+import org.apache.royale.compiler.internal.config.localization.LocalizationManager;
+import org.apache.royale.compiler.internal.workspaces.Workspace;
+import org.apache.royale.compiler.problems.ConfigurationProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.problems.UnexpectedExceptionProblem;
+import org.apache.royale.linter.config.CommandLineConfigurator;
+import org.apache.royale.linter.config.Configuration;
+import org.apache.royale.linter.config.ConfigurationBuffer;
+import org.apache.royale.linter.config.ConfigurationValue;
+import org.apache.royale.linter.config.Configurator;
+import org.apache.royale.linter.config.ILinterSettingsConstants;
+import org.apache.royale.linter.config.LineCommentPosition;
+import org.apache.royale.linter.rules.AnyTypeRule;
+import org.apache.royale.linter.rules.BooleanEqualityRule;
+import org.apache.royale.linter.rules.ClassNameRule;
+import org.apache.royale.linter.rules.ConstantNameRule;
+import org.apache.royale.linter.rules.ConstructorDispatchEventRule;
+import org.apache.royale.linter.rules.ConstructorReturnTypeRule;
+import org.apache.royale.linter.rules.DynamicClassRule;
+import org.apache.royale.linter.rules.EmptyCommentRule;
+import org.apache.royale.linter.rules.EmptyFunctionBodyRule;
+import org.apache.royale.linter.rules.EmptyNestedBlockRule;
+import org.apache.royale.linter.rules.EmptyStatementRule;
+import org.apache.royale.linter.rules.FieldNameRule;
+import org.apache.royale.linter.rules.FunctionNameRule;
+import org.apache.royale.linter.rules.IfBooleanLiteralRule;
+import org.apache.royale.linter.rules.InterfaceNameRule;
+import org.apache.royale.linter.rules.LineCommentPositionRule;
+import org.apache.royale.linter.rules.LocalVarAndParameterNameRule;
+import org.apache.royale.linter.rules.LocalVarShadowsFieldRule;
+import org.apache.royale.linter.rules.MXMLEmptyAttributeRule;
+import org.apache.royale.linter.rules.MXMLIDRule;
+import org.apache.royale.linter.rules.MaxBlockDepthRule;
+import org.apache.royale.linter.rules.MaxParametersRule;
+import org.apache.royale.linter.rules.MissingASDocRule;
+import org.apache.royale.linter.rules.MissingNamespaceRule;
+import org.apache.royale.linter.rules.MissingSemicolonRule;
+import org.apache.royale.linter.rules.MissingTypeRule;
+import org.apache.royale.linter.rules.NumericLeadingZeroesRule;
+import org.apache.royale.linter.rules.OverrideContainsOnlySuperCallRule;
+import org.apache.royale.linter.rules.PackageNameRule;
+import org.apache.royale.linter.rules.StaticConstantsRule;
+import org.apache.royale.linter.rules.StrictEqualityRule;
+import org.apache.royale.linter.rules.StringEventNameRule;
+import org.apache.royale.linter.rules.SwitchWithoutDefaultRule;
+import org.apache.royale.linter.rules.ThisInClosureRule;
+import org.apache.royale.linter.rules.TraceRule;
+import org.apache.royale.linter.rules.WildcardImportRule;
+import org.apache.royale.linter.rules.WithRule;
+import org.apache.royale.utils.FilenameNormalization;
+
+/**
+ * Lints .as and .mxml source files.
+ */
+public class LINTER {
+ private static final String DEFAULT_VAR = "files";
+ private static final String L10N_CONFIG_PREFIX = "org.apache.royale.compiler.internal.config.configuration";
+
+ static enum ExitCode {
+ SUCCESS(0), PRINT_HELP(1), FAILED_WITH_ERRORS(2), FAILED_WITH_EXCEPTIONS(3), FAILED_WITH_CONFIG_PROBLEMS(4);
+
+ ExitCode(int code) {
+ this.code = code;
+ }
+
+ final int code;
+
+ int getCode() {
+ return code;
+ }
+ }
+
+ /**
+ * Java program entry point.
+ *
+ * @param args command line arguments
+ */
+ public static void main(final String[] args) {
+ LINTER formatter = new LINTER();
+ int exitCode = formatter.execute(args);
+ System.exit(exitCode);
+ }
+
+ public LINTER() {
+
+ }
+
+ private ProblemQuery problemQuery;
+ private List<File> inputFiles = new ArrayList<File>();
+ private Configuration configuration;
+ private ConfigurationBuffer configBuffer;
+
+ private LinterSettings settings = new LinterSettings();
+
+ public int execute(String[] args) {
+ ExitCode exitCode = ExitCode.SUCCESS;
+ problemQuery = new ProblemQuery();
+ problemQuery.setShowWarnings(false);
+
+ try {
+ boolean continueLinting = configure(args, problemQuery);
+ if (continueLinting) {
+ for (File inputFile : inputFiles) {
+ String filePath = FilenameNormalization.normalize(inputFile.getAbsolutePath());
+ FileSpecification fileSpec = new FileSpecification(filePath);
+ String fileText = IOUtils.toString(fileSpec.createReader());
+ lintFileText(filePath, fileText, problemQuery.getProblems());
+ }
+ } else if (problemQuery.hasFilteredProblems()) {
+ exitCode = ExitCode.FAILED_WITH_CONFIG_PROBLEMS;
+ } else {
+ exitCode = ExitCode.PRINT_HELP;
+ }
+ } catch (Exception e) {
+ problemQuery.add(new UnexpectedExceptionProblem(e));
+ System.err.println(e.getMessage());
+ exitCode = ExitCode.FAILED_WITH_EXCEPTIONS;
+ } finally {
+ if (problemQuery.hasFilteredProblems()) {
+ final Workspace workspace = new Workspace();
+ final CompilerProblemCategorizer categorizer = new CompilerProblemCategorizer();
+ final ProblemFormatter formatter = new WorkspaceProblemFormatter(workspace, categorizer);
+ final ProblemPrinter printer = new ProblemPrinter(formatter);
+ printer.printProblems(problemQuery.getFilteredProblems());
+ }
+ }
+ return exitCode.getCode();
+ }
+
+ /**
+ * Get the start up message that contains the program name with the copyright
+ * notice.
+ *
+ * @return The startup message.
+ */
+ protected String getStartMessage() {
+ // This message should not be localized.
+ String message = "Apache Royale ActionScript Linter (aslint)\n" + VersionInfo.buildMessage()
+ + "\n";
+ return message;
+ }
+
+ /**
+ * Get my program name.
+ *
+ * @return always "aslint".
+ */
+ protected String getProgramName() {
+ return "aslint";
+ }
+
+ /**
+ * Print detailed help information if -help is provided.
+ */
+ private void processHelp(final List<ConfigurationValue> helpVar) {
+ final Set<String> keywords = new LinkedHashSet<String>();
+ if (helpVar != null) {
+ for (final ConfigurationValue val : helpVar) {
+ for (final Object element : val.getArgs()) {
+ String keyword = (String) element;
+ while (keyword.startsWith("-"))
+ keyword = keyword.substring(1);
+ keywords.add(keyword);
+ }
+ }
+ }
+
+ if (keywords.size() == 0)
+ keywords.add("help");
+
+ final String usages = CommandLineConfigurator.usage(getProgramName(), DEFAULT_VAR, configBuffer, keywords,
+ LocalizationManager.get(), L10N_CONFIG_PREFIX);
+ System.out.println(getStartMessage());
+ System.out.println(usages);
+ }
+
+ private boolean configure(String[] args, ProblemQuery problems) {
+ try {
+ if (args.length == 0) {
+ System.out.println(getStartMessage());
+ return false;
+ }
+ Configurator configurator = new Configurator();
+ configurator.setConfiguration(args, ILinterSettingsConstants.FILES);
+ configuration = configurator.getConfiguration();
+ configBuffer = configurator.getConfigurationBuffer();
+
+ problems.addAll(configurator.getConfigurationProblems());
+
+ if (configBuffer.getVar("version") != null) {
+ System.out.println(VersionInfo.buildMessage());
+ return false;
+ }
+
+ // // Print help if "-help" is present.
+ final List<ConfigurationValue> helpVar = configBuffer.getVar("help");
+ if (helpVar != null || args.length == 0) {
+ processHelp(helpVar);
+ return false;
+ }
+
+ if (problems.hasErrors()) {
+ return false;
+ }
+
+ settings = new LinterSettings();
+ settings.ignoreProblems = configuration.getIgnoreParsingProblems();
+
+ List<LinterRule> rules = new ArrayList<LinterRule>();
+ if (configuration.getAnyType()) {
+ rules.add(new AnyTypeRule());
+ }
+ if (configuration.getBooleanEquality()) {
+ rules.add(new BooleanEqualityRule());
+ }
+ if (configuration.getClassName()) {
+ rules.add(new ClassNameRule());
+ }
+ if (configuration.getConstantName()) {
+ rules.add(new ConstantNameRule());
+ }
+ if (configuration.getConstructorReturnType()) {
+ rules.add(new ConstructorReturnTypeRule());
+ }
+ if (configuration.getConstructorDispatchEvent()) {
+ rules.add(new ConstructorDispatchEventRule());
+ }
+ if (configuration.getDynamicClass()) {
+ rules.add(new DynamicClassRule());
+ }
+ if (configuration.getThisClosure()) {
+ rules.add(new ThisInClosureRule());
+ }
+ if (configuration.getEmptyFunctionBody()) {
+ rules.add(new EmptyFunctionBodyRule());
+ }
+ if (configuration.getEmptyNestedBlock()) {
+ rules.add(new EmptyNestedBlockRule());
+ }
+ if (configuration.getFunctionName()) {
+ rules.add(new FunctionNameRule());
+ }
+ if (configuration.getFieldName()) {
+ rules.add(new FieldNameRule());
+ }
+ if (configuration.getOverrideSuper()) {
+ rules.add(new OverrideContainsOnlySuperCallRule());
+ }
+ if (configuration.getEmptyComment()) {
+ rules.add(new EmptyCommentRule());
+ }
+ if (configuration.getEmptyStatement()) {
+ rules.add(new EmptyStatementRule());
+ }
+ if (configuration.getIfBoolean()) {
+ rules.add(new IfBooleanLiteralRule());
+ }
+ if (configuration.getInterfaceName()) {
+ rules.add(new InterfaceNameRule());
+ }
+ if (configuration.getLeadingZero()) {
+ rules.add(new NumericLeadingZeroesRule());
+ }
+ if (configuration.getLineCommentPosition() != null) {
+ LineCommentPositionRule rule = new LineCommentPositionRule();
+ rule.position = LineCommentPosition.valueOf(configuration.getLineCommentPosition().toUpperCase());
+ rules.add(rule);
+ }
+ if (configuration.getLocalVarParamName()) {
+ rules.add(new LocalVarAndParameterNameRule());
+ }
+ if (configuration.getLocalVarShadowsField()) {
+ rules.add(new LocalVarShadowsFieldRule());
+ }
+ if (configuration.getMaxParams() > 0) {
+ MaxParametersRule rule = new MaxParametersRule();
+ rule.maximum = configuration.getMaxParams();
+ rules.add(rule);
+ }
+ if (configuration.getMaxBlockDepth() > 0) {
+ MaxBlockDepthRule rule = new MaxBlockDepthRule();
+ rule.maximum = configuration.getMaxBlockDepth();
+ rules.add(rule);
+ }
+ if (configuration.getMissingAsdoc()) {
+ rules.add(new MissingASDocRule());
+ }
+ if (configuration.getMissingNamespace()) {
+ rules.add(new MissingNamespaceRule());
+ }
+ if (configuration.getMissingSemicolon()) {
+ rules.add(new MissingSemicolonRule());
+ }
+ if (configuration.getMissingType()) {
+ rules.add(new MissingTypeRule());
+ }
+ if (configuration.getMxmlId()) {
+ rules.add(new MXMLIDRule());
+ }
+ if (configuration.getMxmlEmptyAttr()) {
+ rules.add(new MXMLEmptyAttributeRule());
+ }
+ if (configuration.getPackageName()) {
+ rules.add(new PackageNameRule());
+ }
+ if (configuration.getStaticConstants()) {
+ rules.add(new StaticConstantsRule());
+ }
+ if (configuration.getStrictEquality()) {
+ rules.add(new StrictEqualityRule());
+ }
+ if (configuration.getStringEvent()) {
+ rules.add(new StringEventNameRule());
+ }
+ if (configuration.getSwitchDefault()) {
+ rules.add(new SwitchWithoutDefaultRule());
+ }
+ if (configuration.getTrace()) {
+ rules.add(new TraceRule());
+ }
+ if (configuration.getWildcardImport()) {
+ rules.add(new WildcardImportRule());
+ }
+ if (configuration.getWith()) {
+ rules.add(new WithRule());
+ }
+ settings.rules = rules;
+ if (rules.size() == 0) {
+ ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, "No linter rules were specified");
+ problems.add(problem);
+ return false;
+ }
+
+ for (String filePath : configuration.getFiles()) {
+ File inputFile = new File(filePath);
+ if (!inputFile.exists()) {
+ ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, "Input file does not exist: " + filePath);
+ problems.add(problem);
+ return false;
+ }
+ if (inputFile.isDirectory()) {
+ addDirectory(inputFile);
+ } else {
+ inputFiles.add(inputFile);
+ }
+ }
+ if (inputFiles.size() == 0) {
+ ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, "Missing input file(s)");
+ problems.add(problem);
+ return false;
+ }
+ return true;
+ } catch (Exception e) {
+ e.printStackTrace(System.err);
+ final ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, e.getMessage());
+ problems.add(problem);
+ return false;
+ }
+ }
+
+ private void addDirectory(File inputFile) {
+ for (File file : inputFile.listFiles()) {
+ String fileName = file.getName();
+ if (fileName.startsWith(".")) {
+ continue;
+ }
+ if (file.isDirectory()) {
+ addDirectory(file);
+ } else if (fileName.endsWith(".as") || fileName.endsWith(".mxml")) {
+ inputFiles.add(file);
+ }
+ }
+ }
+
+ public void lintFile(File file, Collection<ICompilerProblem> problems) throws IOException {
+ String filePath = FilenameNormalization.normalize(file.getAbsolutePath());
+ FileSpecification fileSpec = new FileSpecification(filePath);
+ String fileText = IOUtils.toString(fileSpec.createReader());
+ lintFileText(filePath, fileText, problems);
+ }
+
+ public void lintFileText(String filePath, String text, Collection<ICompilerProblem> problems) {
+ filePath = FilenameNormalization.normalize(filePath);
+ if (filePath.endsWith(".mxml")) {
+ lintMXMLTextInternal(filePath, text, problems);
+ } else {
+ lintAS3TextInternal(filePath, text, problems);
+ }
+ }
+
+ public void lintActionScriptText(String text, Collection<ICompilerProblem> problems) {
+ String filePath = FilenameNormalization.normalize("stdin.as");
+ lintAS3TextInternal(filePath, text, problems);
+ }
+
+ public void lintMXMLText(String text, Collection<ICompilerProblem> problems) {
+ String filePath = FilenameNormalization.normalize("stdin.mxml");
+ lintMXMLTextInternal(filePath, text, problems);
+ }
+
+ private void lintAS3TextInternal(String filePath, String text, Collection<ICompilerProblem> problems) {
+ ASLinter linter = new ASLinter(settings);
+ linter.lint(filePath, text, problems);
+ }
+
+ private void lintMXMLTextInternal(String filePath, String text, Collection<ICompilerProblem> problems) {
+ MXMLLinter linter = new MXMLLinter(settings);
+ linter.lint(filePath, text, problems);
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/LinterRule.java b/linter/src/main/java/org/apache/royale/linter/LinterRule.java
new file mode 100644
index 0000000..6f14203
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/LinterRule.java
@@ -0,0 +1,44 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.parsing.IMXMLToken;
+
+public abstract class LinterRule {
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ return null;
+ }
+
+ public Map<Integer /* ASTokenTypes */, TokenVisitor> getTokenVisitors() {
+ return null;
+ }
+
+ public Map<IMXMLToken.MXMLTokenKind, MXMLTokenVisitor> getMXMLTokenVisitors() {
+ return null;
+ }
+
+ public List<MXMLTagVisitor> getMXMLTagVisitors() {
+ return null;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/LinterSettings.java b/linter/src/main/java/org/apache/royale/linter/LinterSettings.java
new file mode 100644
index 0000000..104ad92
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/LinterSettings.java
@@ -0,0 +1,28 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LinterSettings {
+ public List<LinterRule> rules = new ArrayList<LinterRule>();
+ public boolean ignoreProblems = false;
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/MXMLLinter.java b/linter/src/main/java/org/apache/royale/linter/MXMLLinter.java
new file mode 100644
index 0000000..5f4784a
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/MXMLLinter.java
@@ -0,0 +1,182 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.io.StringReader;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.royale.compiler.common.PrefixMap;
+import org.apache.royale.compiler.internal.filespecs.StringFileSpecification;
+import org.apache.royale.compiler.internal.mxml.MXMLData;
+import org.apache.royale.compiler.internal.parsing.mxml.MXMLToken;
+import org.apache.royale.compiler.internal.parsing.mxml.MXMLTokenizer;
+import org.apache.royale.compiler.mxml.IMXMLLanguageConstants;
+import org.apache.royale.compiler.mxml.IMXMLTagData;
+import org.apache.royale.compiler.mxml.IMXMLUnitData;
+import org.apache.royale.compiler.parsing.IMXMLToken;
+import org.apache.royale.compiler.parsing.IMXMLToken.MXMLTokenKind;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.internal.BaseLinter;
+
+public class MXMLLinter extends BaseLinter {
+ private static final String LINTER_TAG_OFF = "@linter:off";
+ private static final String LINTER_TAG_ON = "@linter:on";
+
+ public MXMLLinter(LinterSettings settings) {
+ super(settings);
+ }
+
+ public void lint(String filePath, String text, Collection<ICompilerProblem> allProblems) {
+ if (allProblems == null) {
+ allProblems = new ArrayList<ICompilerProblem>();
+ }
+ List<ICompilerProblem> fileProblems = new ArrayList<ICompilerProblem>();
+ try {
+ StringReader textReader = new StringReader(text);
+ MXMLTokenizer mxmlTokenizer = new MXMLTokenizer();
+ List<MXMLToken> originalTokens = null;
+ try {
+ originalTokens = mxmlTokenizer.parseTokens(textReader);
+ } finally {
+ IOUtils.closeQuietly(textReader);
+ IOUtils.closeQuietly(mxmlTokenizer);
+ }
+
+ if (mxmlTokenizer.hasTokenizationProblems()) {
+ fileProblems.addAll(mxmlTokenizer.getTokenizationProblems());
+ }
+
+ if (!settings.ignoreProblems && hasErrors(fileProblems)) {
+ return;
+ }
+ boolean skipLinting = false;
+ IMXMLToken[] allTokens = originalTokens.toArray(new IMXMLToken[0]);
+ MXMLTokenQuery tokenQuery = new MXMLTokenQuery(allTokens);
+ for (LinterRule rule : settings.rules) {
+ Map<MXMLTokenKind, MXMLTokenVisitor> tokenHandlers = rule.getMXMLTokenVisitors();
+ if (tokenHandlers != null) {
+ for (IMXMLToken token : originalTokens) {
+ MXMLTokenKind tokenKind = token.getMXMLTokenKind();
+ if (tokenKind == MXMLTokenKind.COMMENT) {
+ String tokenText = token.getText();
+ boolean isASDoc = tokenText.startsWith("<!---") && tokenText.length() > 7;
+ String trimmed = tokenText.substring(isASDoc ? 5 : 4, tokenText.length() - 3).trim();
+ if (!skipLinting && LINTER_TAG_OFF.equals(trimmed)) {
+ skipLinting = true;
+ } else if (skipLinting && LINTER_TAG_ON.equals(trimmed)) {
+ skipLinting = false;
+ continue;
+ }
+ }
+ if (skipLinting) {
+ continue;
+ }
+ if (tokenHandlers.containsKey(tokenKind)) {
+ tokenHandlers.get(tokenKind).visit(token, tokenQuery, fileProblems);
+ }
+ }
+ }
+ }
+
+ PrefixMap prefixMap = mxmlTokenizer.getPrefixMap();
+ MXMLData mxmlData = new MXMLData(originalTokens, prefixMap, new StringFileSpecification(filePath, text));
+ IMXMLTagData rootTag = mxmlData.getRootTag();
+ visitTag(rootTag, tokenQuery, fileProblems);
+ IMXMLUnitData current = rootTag;
+ int offset = 1;
+ String className = Paths.get(filePath).getFileName().toString();
+ int extensionIndex = className.indexOf('.');
+ if (extensionIndex != -1) {
+ className = className.substring(0, extensionIndex);
+ }
+ String componentName = mxmlData.getRootTag().getShortName();
+ StringBuilder builder = new StringBuilder();
+ builder.append("/* ");
+ builder.append(LINTER_TAG_OFF);
+ builder.append(" */");
+ builder.append("package{public class ");
+ builder.append(className);
+ builder.append(" extends ");
+ builder.append(componentName);
+ builder.append("{");
+ while (current != null) {
+ if (current instanceof IMXMLTagData) {
+ IMXMLTagData tag = (IMXMLTagData) current;
+ if ("Script".equals(tag.getShortName())) {
+ String prefix = tag.getPrefix();
+ String ns = prefixMap.getNamespaceForPrefix(prefix);
+ if (ns != null
+ && (IMXMLLanguageConstants.NAMESPACE_MXML_2006.equals(ns)
+ || IMXMLLanguageConstants.NAMESPACE_MXML_2009.equals(ns)
+ || IMXMLLanguageConstants.NAMESPACE_MXML_2012.equals(ns))) {
+ if (tag.isOpenTag()) {
+ int line = tag.getLine();
+ String scriptText = tag.getCompilableText();
+ // wrap the script inside a class so that rules don't
+ // get confused. adds new lines to ensure the line
+ // numbers match the original file
+ for (int i = offset; i < line; i++) {
+ builder.append("\n");
+ }
+ builder.append("/* ");
+ builder.append(LINTER_TAG_ON);
+ builder.append(" */\n");
+ builder.append(scriptText);
+ builder.append("/* ");
+ builder.append(LINTER_TAG_OFF);
+ builder.append(" */");
+ } else {
+ offset = tag.getLine() + 1;
+ }
+ }
+ }
+ }
+ current = current.getNext();
+ }
+ builder.append("}}\n");
+ ASLinter asLinter = new ASLinter(settings);
+ asLinter.lint(filePath, builder.toString(), fileProblems);
+ } finally {
+ allProblems.addAll(fileProblems);
+ }
+ }
+
+ private void visitTag(IMXMLTagData tag, MXMLTokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ for (LinterRule rule : settings.rules) {
+ List<MXMLTagVisitor> tagVisitors = rule.getMXMLTagVisitors();
+ if (tagVisitors != null) {
+ for (int i = 0; i < tagVisitors.size(); i++) {
+ tagVisitors.get(i).visit(tag, tokenQuery, problems);
+ }
+ }
+ }
+
+ IMXMLTagData current = tag.getFirstChild(true);
+ while (current != null) {
+ visitTag(current, tokenQuery, problems);
+ current = current.getNextSibling(true);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/MXMLTagVisitor.java b/linter/src/main/java/org/apache/royale/linter/MXMLTagVisitor.java
new file mode 100644
index 0000000..e327d23
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/MXMLTagVisitor.java
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.Collection;
+
+import org.apache.royale.compiler.mxml.IMXMLTagData;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+
+public interface MXMLTagVisitor {
+ void visit(IMXMLTagData tag, MXMLTokenQuery tokenQuery, Collection<ICompilerProblem> problems);
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/MXMLTokenQuery.java b/linter/src/main/java/org/apache/royale/linter/MXMLTokenQuery.java
new file mode 100644
index 0000000..82fb830
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/MXMLTokenQuery.java
@@ -0,0 +1,96 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.List;
+
+import org.apache.royale.compiler.common.ISourceLocation;
+import org.apache.royale.compiler.parsing.IMXMLToken;
+
+public class MXMLTokenQuery {
+ public MXMLTokenQuery(IMXMLToken[] tokens) {
+ allTokens = tokens;
+ }
+
+ private IMXMLToken[] allTokens;
+
+ /**
+ * Returns all tokens in the file.
+ */
+ public IMXMLToken[] getTokens() {
+ return allTokens;
+ }
+
+ /**
+ * Returns the token immediately before a source location.
+ */
+ public IMXMLToken getTokenBefore(ISourceLocation sourceLocation) {
+ return getTokenBefore(sourceLocation, false);
+ }
+
+ /**
+ * Returns the token immediately before a source location, with the option
+ * to skip comment tokens.
+ */
+ public IMXMLToken getTokenBefore(ISourceLocation sourceLocation, boolean skipComments) {
+ IMXMLToken result = null;
+ for (IMXMLToken otherToken : allTokens) {
+ if (skipComments && isComment(otherToken)) {
+ continue;
+ }
+ if (otherToken.getStart() >= sourceLocation.getAbsoluteStart()) {
+ return result;
+ }
+ result = otherToken;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the token immediately after a source location.
+ */
+ public IMXMLToken getTokenAfter(ISourceLocation sourceLocation) {
+ return getTokenAfter(sourceLocation, false);
+ }
+
+ /**
+ * Returns the token immediately after a source location, with the option to
+ * skip comment tokens.
+ */
+ public IMXMLToken getTokenAfter(ISourceLocation sourceLocation, boolean skipComments) {
+ for (IMXMLToken token : allTokens) {
+ if (skipComments && isComment(token)) {
+ continue;
+ }
+ if (token.getStart() >= sourceLocation.getAbsoluteEnd()) {
+ return token;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks if a token is comment.
+ */
+ public boolean isComment(IMXMLToken token) {
+ return token.getMXMLTokenKind() == IMXMLToken.MXMLTokenKind.COMMENT;
+ }
+
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/MXMLTokenVisitor.java b/linter/src/main/java/org/apache/royale/linter/MXMLTokenVisitor.java
new file mode 100644
index 0000000..520a3a6
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/MXMLTokenVisitor.java
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.Collection;
+
+import org.apache.royale.compiler.parsing.IMXMLToken;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+
+public interface MXMLTokenVisitor {
+ void visit(IMXMLToken token, MXMLTokenQuery tokenQuery, Collection<ICompilerProblem> problems);
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/NodeVisitor.java b/linter/src/main/java/org/apache/royale/linter/NodeVisitor.java
new file mode 100644
index 0000000..6e723ec
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/NodeVisitor.java
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.Collection;
+
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.as.IASNode;
+
+public interface NodeVisitor {
+ void visit(IASNode node, TokenQuery tokenQuery, Collection<ICompilerProblem> problems);
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/TokenQuery.java b/linter/src/main/java/org/apache/royale/linter/TokenQuery.java
new file mode 100644
index 0000000..99b69c6
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/TokenQuery.java
@@ -0,0 +1,203 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.royale.compiler.common.ISourceLocation;
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.tree.as.IASNode;
+
+public class TokenQuery {
+ public TokenQuery(IASToken[] tokens) {
+ allTokens = tokens;
+ }
+
+ private IASToken[] allTokens;
+
+ /**
+ * Returns all tokens in the file.
+ */
+ public IASToken[] getTokens() {
+ return allTokens;
+ }
+
+ /**
+ * Returns all tokens inside of a particular node.
+ */
+ public IASToken[] getTokens(IASNode node) {
+ List<IASToken> result = new ArrayList<>();
+ for (IASToken token : allTokens) {
+ if (token.getAbsoluteStart() < node.getAbsoluteStart()) {
+ continue;
+ }
+ if (token.getAbsoluteStart() >= node.getAbsoluteEnd()) {
+ break;
+ }
+ result.add(token);
+ }
+ return result.toArray(new IASToken[0]);
+ }
+
+ /**
+ * Returns the token immediately before a source location.
+ */
+ public IASToken getTokenBefore(ISourceLocation sourceLocation) {
+ return getTokenBefore(sourceLocation, false);
+ }
+
+ /**
+ * Returns the token immediately before a source location, with the option
+ * to skip comment tokens.
+ */
+ public IASToken getTokenBefore(ISourceLocation sourceLocation, boolean skipComments) {
+ IASToken result = null;
+ for (IASToken otherToken : allTokens) {
+ if (skipComments && isComment(otherToken)) {
+ continue;
+ }
+ if (otherToken.getAbsoluteStart() >= sourceLocation.getAbsoluteStart()) {
+ return result;
+ }
+ result = otherToken;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the token immediately after a source location.
+ */
+ public IASToken getTokenAfter(ISourceLocation sourceLocation) {
+ return getTokenAfter(sourceLocation, false);
+ }
+
+ /**
+ * Returns the token immediately after a source location, with the option to
+ * skip comment tokens.
+ */
+ public IASToken getTokenAfter(ISourceLocation sourceLocation, boolean skipComments) {
+ for (IASToken token : allTokens) {
+ if (skipComments && isComment(token)) {
+ continue;
+ }
+ if (token.getAbsoluteStart() >= sourceLocation.getAbsoluteEnd()) {
+ return token;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the first token inside a node.
+ */
+ public IASToken getFirstToken(IASNode node) {
+ for (IASToken token : allTokens) {
+ if (token.getAbsoluteStart() >= node.getAbsoluteStart()) {
+ return token;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the last token inside a node.
+ */
+ public IASToken getLastToken(IASNode node) {
+ IASToken result = null;
+ for (IASToken token : allTokens) {
+ if (token.getAbsoluteStart() >= node.getAbsoluteStart()) {
+ result = token;
+ } else if (result != null) {
+ break;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns all comment tokens inside a node.
+ */
+ public IASToken[] getCommentsInside(IASNode node) {
+ List<IASToken> result = new ArrayList<>();
+ IASToken[] tokensInside = getTokens(node);
+ for (IASToken token : tokensInside) {
+ if (isComment(token)) {
+ result.add(token);
+ }
+ }
+ return result.toArray(new IASToken[0]);
+ }
+
+ /**
+ * Checks if a token is a comment.
+ */
+ public boolean isComment(IASToken token) {
+ return token.getType() == ASTokenTypes.HIDDEN_TOKEN_COMMENT
+ || token.getType() == ASTokenTypes.HIDDEN_TOKEN_SINGLE_LINE_COMMENT
+ || token.getType() == ASTokenTypes.HIDDEN_TOKEN_MULTI_LINE_COMMENT
+ || token.getType() == ASTokenTypes.TOKEN_ASDOC_COMMENT;
+ }
+
+ public IASToken getPreviousTokenOfType(ISourceLocation before, int type) {
+ IASToken result = null;
+ for (IASToken token : allTokens) {
+ if (token.getAbsoluteStart() >= before.getAbsoluteStart()) {
+ return result;
+ }
+ if (token.getType() == type) {
+ result = token;
+ }
+ }
+ return null;
+ }
+
+ public IASToken getNextTokenOfType(ISourceLocation after, int type) {
+ for (IASToken token : allTokens) {
+ if (token.getType() == type && token.getAbsoluteStart() >= after.getAbsoluteEnd()) {
+ return token;
+ }
+ }
+ return null;
+ }
+
+ public IASToken getPreviousComment(ISourceLocation before) {
+ IASToken result = null;
+ for (IASToken token : allTokens) {
+ if (token.getAbsoluteStart() >= before.getAbsoluteStart()) {
+ return result;
+ }
+ if (isComment(token)) {
+ result = token;
+ }
+ }
+ return null;
+ }
+
+ public IASToken getNextComment(ISourceLocation after) {
+ for (IASToken token : allTokens) {
+ if (token.getAbsoluteStart() >= after.getAbsoluteEnd() && isComment(token)) {
+ return token;
+ }
+ }
+ return null;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/TokenVisitor.java b/linter/src/main/java/org/apache/royale/linter/TokenVisitor.java
new file mode 100644
index 0000000..cfc931a
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/TokenVisitor.java
@@ -0,0 +1,29 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter;
+
+import java.util.Collection;
+
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+
+public interface TokenVisitor {
+ void visit(IASToken token, TokenQuery tokenQuery, Collection<ICompilerProblem> problems);
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/CommandLineConfigurator.java b/linter/src/main/java/org/apache/royale/linter/config/CommandLineConfigurator.java
new file mode 100644
index 0000000..f547fa0
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/CommandLineConfigurator.java
@@ -0,0 +1,607 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.TreeSet;
+import java.util.Set;
+import java.util.Iterator;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.HashMap;
+import java.io.File;
+
+import com.google.common.base.Joiner;
+
+import org.apache.royale.compiler.Messages;
+import org.apache.royale.compiler.exceptions.ConfigurationException;
+import org.apache.royale.compiler.internal.config.localization.LocalizationManager;
+
+/**
+ * A utility class, which is used to parse an array of command line args and
+ * populate a ConfigurationBuffer. It also contains some associated methods like
+ * brief() and usage(). A counterpart of FileConfigurator and
+ * SystemPropertyConfigurator.
+ */
+public class CommandLineConfigurator
+{
+ public static final String SOURCE_COMMAND_LINE = "command line";
+
+ /**
+ * parse - buffer up configuration vals from the command line
+ *
+ * @param buffer the configuration buffer to hold the results
+ * @param defaultvar the variable name where the trailing loose args go
+ * @param args the command line
+ */
+ public static void parse(final ConfigurationBuffer buffer,
+ final String defaultvar,
+ final String[] args)
+ throws ConfigurationException
+ {
+ // "no-default-arg" means the application does not have a default var.
+ assert defaultvar == null || buffer.isValidVar(defaultvar) || "no-default-arg".equals(defaultvar) : "coding error: config must provide default var " + defaultvar;
+
+ Map<String, String> aliases = getAliases(buffer);
+ final int START = 1;
+ final int ARGS = 2;
+ final int EXEC = 3;
+ final int DONE = 4;
+
+ int i = 0, iStart = 0, iEnd = 0;
+ String var = null;
+ int varArgCount = -2;
+ List<String> argList = new LinkedList<String>();
+ Set<String> vars = new HashSet<String>();
+ boolean append = false;
+ boolean dash = true;
+
+ int mode = START;
+
+ while (mode != DONE)
+ {
+ switch (mode)
+ {
+ case START:
+ {
+ iStart = i;
+
+ if (args.length == i)
+ {
+ mode = DONE;
+ break;
+ }
+ // expect -var, --, or the beginning of default args
+
+ mode = ARGS;
+ varArgCount = -2;
+
+ if (args[i].equals("--"))
+ {
+ dash = false;
+ if (defaultvar != null)
+ var = defaultvar;
+ else
+ mode = START;
+ ++i;
+ }
+ else if (dash && args[i].startsWith("+"))
+ {
+ String token = null;
+ int c = (args[i].length() > 1 && args[i].charAt(1) == '+') ? 2 : 1; // gnu-style?
+
+ int equals = args[i].indexOf('=');
+ String rest = null;
+ if (equals != -1)
+ {
+ rest = args[i].substring(equals + 1);
+ token = args[i++].substring(c, equals);
+ }
+ else
+ {
+ token = args[i++].substring(c);
+ }
+ if (equals != -1)
+ {
+ iEnd = i;
+ buffer.setToken(token, rest);
+ buffer.addPosition(token, iStart, iEnd);
+ }
+ else
+ {
+ if (i == args.length)
+ {
+ throw new ConfigurationException.Token(ConfigurationException.Token.INSUFFICIENT_ARGS,
+ token, var, source, -1);
+ }
+ rest = args[i++];
+ iEnd = i;
+ buffer.setToken(token, rest);
+ buffer.addPosition(token, iStart, iEnd);
+ }
+ mode = START;
+ break;
+ }
+ else if (dash && isAnArgument(args[i]))
+ {
+ int c = (args[i].length() > 1 && args[i].charAt(1) == '-') ? 2 : 1; // gnu-style?
+
+ int plusequals = args[i].indexOf("+=");
+ int equals = args[i].indexOf('=');
+ String rest = null;
+ if (plusequals != -1)
+ {
+ rest = args[i].substring(plusequals + 2);
+ var = args[i++].substring(c, plusequals);
+ append = true;
+ }
+ else if (equals != -1)
+ {
+ rest = args[i].substring(equals + 1);
+ var = args[i++].substring(c, equals);
+ }
+ else
+ {
+ var = args[i++].substring(c);
+ }
+
+ if (aliases.containsKey(var))
+ var = aliases.get(var);
+
+ if (!buffer.isValidVar(var))
+ {
+ throw new ConfigurationException.UnknownVariable(var, source, -1);
+ }
+
+ if (equals != -1)
+ {
+ if ((rest == null) || (rest.length() == 0))
+ {
+ varArgCount = -1;
+ mode = EXEC;
+ }
+ else
+ {
+ String seps = null;
+ if (buffer.getInfo(var).isPath())
+ {
+ seps = "[," + File.pathSeparatorChar + "]";
+ }
+ else
+ {
+ seps = ",";
+ }
+
+ String[] tokens = rest.split(seps);
+ argList.addAll(Arrays.asList(tokens));
+ varArgCount = buffer.getVarArgCount(var);
+ mode = EXEC;
+ }
+ }
+
+ }
+ else
+ {
+ // asdoc sets default var as no-default-arg - it has no default vars
+ if (defaultvar != null && !defaultvar.equals("no-default-arg"))
+ {
+ // don't increment i, let ARGS pick it up.
+ var = defaultvar;
+ }
+ else
+ {
+ throw new ConfigurationException.UnexpectedDefaults(null, null, -1);
+ }
+ }
+ break;
+ }
+ case ARGS:
+ {
+ if (varArgCount == -2)
+ {
+ if (isBoolean(buffer, var))
+ {
+ varArgCount = 0;
+ mode = EXEC;
+ break;
+ }
+ else
+ {
+ varArgCount = buffer.getVarArgCount(var);
+ }
+ }
+ assert varArgCount >= -1; // just in case the getVarArgCount author was insane.
+
+ if (args.length == i)
+ {
+ mode = EXEC;
+ break;
+ }
+
+ boolean greedy = buffer.getInfo(var).isGreedy();
+
+ // accumulating non-command arguments...
+
+ // check for a terminator on our accumulated parameter list
+ if (!greedy && dash && isAnArgument(args[i]))
+ {
+ if (varArgCount == -1)
+ {
+ // we were accumulating an unlimited set of args, a new var terminates that.
+ mode = EXEC;
+ break;
+ }
+ throw new ConfigurationException.IncorrectArgumentCount(varArgCount, argList.size(), var, source, -1);
+ }
+
+ argList.add(args[i++]);
+ if (argList.size() == varArgCount)
+ {
+ mode = EXEC;
+ }
+
+ break;
+ }
+ case EXEC:
+ {
+ if ((varArgCount != -1) && (argList.size() != varArgCount))
+ {
+ throw new ConfigurationException.IncorrectArgumentCount(varArgCount, argList.size(), var, source, -1);
+ }
+ if (varArgCount == 0) // boolean flag fakery...
+ argList.add("true");
+
+ if (vars.contains(var))
+ {
+ if ((defaultvar != null) && var.equals(defaultvar))
+ {
+ // we could perhaps accumulate the defaults spread out through
+ // the rest of the flags, but for now we'll call this illegal.
+ throw new ConfigurationException.InterspersedDefaults(var, source, -1);
+ }
+ }
+ iEnd = i;
+ buffer.setVar(var, new LinkedList<String>(argList), source, -1, null, append);
+ buffer.addPosition(var, iStart, iEnd);
+ append = false;
+ vars.add(var);
+ argList.clear();
+ mode = START;
+ break;
+ }
+ case DONE:
+ {
+ assert false;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a string like "-foo" or "-5" or "-123.mxml", this determines
+ * whether the string is an argument or... not an argument (e.g. numeral)
+ */
+ private static boolean isAnArgument(final String arg)
+ {
+ return (arg.startsWith("-") &&
+ // if the first character after a dash is numeric, this is not
+ // an argument, it is a parameter (and therefore non-terminating)
+ (arg.length() > 1) && !Character.isDigit(arg.charAt(1)));
+ }
+
+ private static Map<String, String> getAliases(ConfigurationBuffer buffer)
+ {
+ Map<String, String> aliases = new HashMap<String, String>();
+ aliases.putAll(buffer.getAliases());
+ for (final String varname : buffer.getVars())
+ {
+ if (varname.indexOf('.') == -1)
+ continue;
+
+ String leafname = varname.substring(varname.lastIndexOf('.') + 1);
+ if (aliases.containsKey(leafname))
+ continue;
+ aliases.put(leafname, varname);
+ }
+
+ return aliases;
+ }
+
+ private static boolean isBoolean(ConfigurationBuffer buffer, String var)
+ {
+ ConfigurationInfo info = buffer.getInfo(var);
+
+ if (info.getArgCount() > 1)
+ return false;
+
+ Class<?> c = info.getArgType(0);
+
+ return ((c == boolean.class) || (c == Boolean.class));
+ }
+
+ public static String brief(String program, String defaultvar, LocalizationManager l10n, String l10nPrefix)
+ {
+ Map<String, Object> params = new HashMap<String, Object>();
+ params.put("defaultVar", defaultvar);
+ params.put("program", program);
+ return l10n.getLocalizedTextString(l10nPrefix + ".Brief", params);
+ }
+
+ public static String usage(String program, String defaultVar, ConfigurationBuffer cfgbuf, Set<String> keywords, LocalizationManager lmgr, String l10nPrefix)
+ {
+ boolean isCompc = program.contains("compc");
+ Map<String, String> aliases = getAliases(cfgbuf);
+ Map<String, String> sesaila = new HashMap<String, String>();
+ for (Iterator<Map.Entry<String, String>> it = aliases.entrySet().iterator(); it.hasNext();)
+ {
+ Map.Entry<String, String> e = it.next();
+ sesaila.put(e.getValue(), e.getKey());
+ }
+
+ TreeSet<String> printSet = new TreeSet<String>();
+
+ boolean all = false;
+ boolean advanced = false;
+ boolean details = false;
+ boolean syntax = false;
+ boolean printaliases = false;
+
+ // figure out behavior..
+ Set<String> newSet = new HashSet<String>();
+ for (Iterator<String> kit = keywords.iterator(); kit.hasNext();)
+ {
+ String keyword = kit.next();
+
+ if (keyword.equals("list"))
+ {
+ all = true;
+ newSet.add("*");
+ }
+ else if (keyword.equals("advanced"))
+ {
+ advanced = true;
+ if (keywords.size() == 1)
+ {
+ all = true;
+ newSet.add("*");
+ }
+ }
+ else if (keyword.equals("details"))
+ {
+ details = true;
+ }
+ else if (keyword.equals("syntax"))
+ {
+ syntax = true;
+ }
+ else if (keyword.equals("aliases"))
+ {
+ printaliases = true;
+ }
+ else
+ {
+ details = true;
+ newSet.add(keyword);
+ }
+ }
+ if (syntax)
+ {
+ final List<String> lines = ConfigurationBuffer.formatText(getSyntaxDescription(program, defaultVar, advanced, lmgr, l10nPrefix), 78);
+ return Joiner.on("\n").join(lines);
+ }
+ keywords = newSet;
+
+ // accumulate set to print
+ for (Iterator<String> kit = keywords.iterator(); kit.hasNext();)
+ {
+ String keyword = kit.next().toLowerCase();
+
+ for (final String var : cfgbuf.getVars())
+ {
+ ConfigurationInfo info = cfgbuf.getInfo(var);
+
+ // If the client is not "compc", skip "compc-only" options.
+ if (info.isCompcOnly && !isCompc)
+ continue;
+
+ String description = getDescription(cfgbuf, var, lmgr, l10nPrefix);
+
+ if ((all
+ || (var.indexOf(keyword) != -1)
+ || ((description != null) && (description.toLowerCase().indexOf(keyword) != -1))
+ || (keyword.matches(var))
+ || ((sesaila.get(var) != null) && (sesaila.get(var)).indexOf(keyword) != -1))
+ && (!info.isHidden())
+ && (!info.isRemoved())
+ && (advanced || !info.isAdvanced()))
+ {
+ if (printaliases && sesaila.containsKey(var))
+ printSet.add(sesaila.get(var));
+ else
+ printSet.add(var);
+ }
+ else
+ {
+ /*
+ * for (int i = 0; i < info.getAliases().length; ++i) {
+ * String alias = info.getAliases()[i]; if (alias.indexOf(
+ * keyword ) != -1) { printSet.add( var ); } }
+ */
+ }
+ }
+ }
+
+ StringBuilder output = new StringBuilder(1024);
+
+ if (printSet.size() == 0)
+ {
+ String nkm = lmgr.getLocalizedTextString(l10nPrefix + ".NoKeywordsMatched");
+ output.append(nkm);
+ output.append("\n");
+ }
+ else
+ for (Iterator<String> it = printSet.iterator(); it.hasNext();)
+ {
+ String avar = it.next();
+ String var = avar;
+ if (aliases.containsKey(avar))
+ var = aliases.get(avar);
+
+ ConfigurationInfo info = cfgbuf.getInfo(var);
+ assert info != null;
+
+ output.append("-");
+ output.append(avar);
+
+ int count = cfgbuf.getVarArgCount(var);
+ if ((count >= 1) && (!isBoolean(cfgbuf, var)))
+ {
+ for (int i = 0; i < count; ++i)
+ {
+ output.append(" <");
+ output.append(cfgbuf.getVarArgName(var, i));
+ output.append(">");
+ }
+ }
+ else if (count == -1)
+ {
+ String last = "";
+ for (int i = 0; i < 5; ++i)
+ {
+ String argname = cfgbuf.getVarArgName(var, i);
+ if (!argname.equals(last))
+ {
+ output.append(" [");
+ output.append(argname);
+ output.append("]");
+ last = argname;
+ }
+ else
+ {
+ output.append(" [...]");
+ break;
+ }
+ }
+ }
+
+ output.append("\n");
+
+ if (details)
+ {
+ StringBuilder description = new StringBuilder(160);
+ if (printaliases)
+ {
+ if (aliases.containsKey(avar))
+ {
+ String fullname = lmgr.getLocalizedTextString(l10nPrefix + ".FullName");
+ description.append(fullname);
+ description.append(" -");
+ description.append(aliases.get(avar));
+ description.append("\n");
+ }
+ }
+ else if (sesaila.containsKey(var))
+ {
+ String alias = lmgr.getLocalizedTextString(l10nPrefix + ".Alias");
+ description.append(alias);
+ description.append(" -");
+ description.append(sesaila.get(var));
+ description.append("\n");
+ }
+
+ String d = getDescription(cfgbuf, var, lmgr, l10nPrefix);
+ if (var.equals("help") && (printSet.size() > 2))
+ {
+ String helpKeywords = lmgr.getLocalizedTextString(l10nPrefix + ".HelpKeywords");
+ description.append(helpKeywords);
+ }
+ else if (d != null)
+ description.append(d);
+
+ String flags = "";
+ if (info.isAdvanced())
+ {
+ String advancedString = lmgr.getLocalizedTextString(l10nPrefix + ".Advanced");
+ flags += (((flags.length() == 0) ? " (" : ", ") + advancedString);
+ }
+ if (info.allowMultiple())
+ {
+ String repeatableString = lmgr.getLocalizedTextString(l10nPrefix + ".Repeatable");
+ flags += (((flags.length() == 0) ? " (" : ", ") + repeatableString);
+ }
+ if ((defaultVar != null) && var.equals(defaultVar))
+ {
+ String defaultString = lmgr.getLocalizedTextString(l10nPrefix + ".Default");
+ flags += (((flags.length() == 0) ? " (" : ", ") + defaultString);
+ }
+ if (info.isRoyaleOnly())
+ {
+ String royaleOnlylString = Messages.getString("RoyaleOnly");
+ flags += (((flags.length() == 0) ? " (" : ", ") + royaleOnlylString);
+ }
+ if (flags.length() != 0)
+ {
+ flags += ")";
+ }
+ description.append(flags);
+
+ List<String> descriptionLines = ConfigurationBuffer.formatText(description.toString(), 70);
+
+ for (final String next : descriptionLines)
+ {
+ output.append(" ");
+ output.append(next);
+ output.append("\n");
+ }
+ }
+ }
+ return output.toString();
+ }
+
+ public static String getDescription(ConfigurationBuffer buffer, String var, LocalizationManager l10n, String l10nPrefix)
+ {
+ String key = (l10nPrefix == null) ? var : (l10nPrefix + "." + var);
+ String description = l10n.getLocalizedTextString(key, null);
+
+ return description;
+ }
+
+ public static String getSyntaxDescription(String program, String defaultVar, boolean advanced, LocalizationManager l10n, String l10nPrefix)
+ {
+ Map<String, Object> params = new HashMap<String, Object>();
+ params.put("defaultVar", defaultVar);
+ params.put("program", program);
+
+ String key = l10nPrefix + "." + (advanced ? "AdvancedSyntax" : "Syntax");
+ String text = l10n.getLocalizedTextString(key, params);
+
+ if (text == null)
+ {
+ text = "No syntax help available, try '-help list' to list available configuration variables.";
+ assert false : "Localized text for syntax description not found!";
+ }
+ return text;
+ }
+
+ public static final String source = SOURCE_COMMAND_LINE;
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/Configuration.java b/linter/src/main/java/org/apache/royale/linter/config/Configuration.java
new file mode 100644
index 0000000..32b82cf
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/Configuration.java
@@ -0,0 +1,764 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.royale.compiler.exceptions.ConfigurationException;
+import org.apache.royale.compiler.internal.config.annotations.Arguments;
+import org.apache.royale.compiler.internal.config.annotations.Config;
+import org.apache.royale.compiler.internal.config.annotations.InfiniteArguments;
+import org.apache.royale.compiler.internal.config.annotations.Mapping;
+import org.apache.royale.compiler.problems.DeprecatedConfigurationOptionProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.problems.RemovedConfigurationOptionProblem;
+
+public class Configuration {
+
+ private static Map<String, String> aliases = null;
+
+ public static Map<String, String> getAliases() {
+ if (aliases == null) {
+ aliases = new HashMap<String, String>();
+
+ // aliases.put("s", "short-option");
+ }
+ return aliases;
+ }
+
+ /**
+ * Collection of fatal and non-fatal configuration problems.
+ */
+ private Collection<ICompilerProblem> configurationProblems = new ArrayList<ICompilerProblem>();
+
+ /**
+ * Get the configuration problems. This should be called after the configuration
+ * has been processed.
+ *
+ * @return a collection of fatal and non-fatal configuration problems.
+ */
+ public Collection<ICompilerProblem> getConfigurationProblems() {
+ return configurationProblems;
+ }
+
+ /**
+ * Validate configuration options values.
+ *
+ * @param configurationBuffer Configuration buffer.
+ * @throws ConfigurationException Error.
+ */
+ public void validate(ConfigurationBuffer configurationBuffer) throws ConfigurationException {
+ // process the merged configuration buffer. right, can't just process the args.
+ processDeprecatedAndRemovedOptions(configurationBuffer);
+ }
+
+ private void processDeprecatedAndRemovedOptions(ConfigurationBuffer configurationBuffer) {
+ for (final String var : configurationBuffer.getVars()) {
+ ConfigurationInfo info = configurationBuffer.getInfo(var);
+ List<ConfigurationValue> values = configurationBuffer.getVar(var);
+ if (values != null) {
+ for (final ConfigurationValue cv : values) {
+ if (info.isRemoved()) {
+ addRemovedConfigurationOptionProblem(cv);
+ } else if (info.isDeprecated() && configurationBuffer.getVar(var) != null) {
+ String replacement = info.getDeprecatedReplacement();
+ String since = info.getDeprecatedSince();
+ DeprecatedConfigurationOptionProblem problem = new DeprecatedConfigurationOptionProblem(var,
+ replacement, since, cv.getSource(), cv.getLine());
+ configurationProblems.add(problem);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a RemovedConfigurationOptionProblem to the list of configuration
+ * problems.
+ *
+ * @param cv
+ */
+ private void addRemovedConfigurationOptionProblem(ConfigurationValue cv) {
+ RemovedConfigurationOptionProblem problem = new RemovedConfigurationOptionProblem(cv.getVar(), cv.getSource(),
+ cv.getLine());
+ configurationProblems.add(problem);
+ }
+
+ //
+ // 'help' option from CommandLineConfiguration
+ //
+
+ /**
+ * dummy, just a trigger for help text
+ */
+ @Config(displayed = false, greedy = true)
+ @Arguments("keyword")
+ @InfiniteArguments
+ public void setHelp(ConfigurationValue cv, String[] keywords) {
+
+ }
+
+ //
+ // 'version' option from CommandLineConfiguration
+ //
+
+ /**
+ * Dummy option. Just a trigger for version info.
+ */
+ @Config
+ public void setVersion(ConfigurationValue cv, boolean value) {
+ }
+
+ //
+ // 'files' option
+ //
+
+ private List<String> files = new ArrayList<String>();
+
+ /**
+ * @return A list of filespecs. It's the default variable for command line.
+ */
+ public List<String> getFiles() {
+ return files;
+ }
+
+ @Config(allowMultiple = true, hidden = true)
+ @Mapping("files")
+ @Arguments(Arguments.PATH_ELEMENT)
+ @InfiniteArguments
+ public void setFiles(ConfigurationValue cv, List<String> args) throws ConfigurationException {
+ this.files.addAll(args);
+ }
+
+ //
+ // 'ignore-parsing-problems' option
+ //
+
+ private boolean ignoreParsingProblems = false;
+
+ public boolean getIgnoreParsingProblems() {
+ return ignoreParsingProblems;
+ }
+
+ @Config(advanced = true)
+ @Mapping("ignore-parsing-problems")
+ public void setIgnoreParsingProblems(ConfigurationValue cv, boolean b) {
+ this.ignoreParsingProblems = b;
+ }
+
+ //
+ // 'any-type' option
+ //
+
+ private boolean anyType = false;
+
+ public boolean getAnyType() {
+ return anyType;
+ }
+
+ @Config
+ @Mapping("any-type")
+ public void setAnyType(ConfigurationValue cv, boolean b) {
+ this.anyType = b;
+ }
+
+ //
+ // 'boolean-equality' option
+ //
+
+ private boolean booleanEquality = false;
+
+ public boolean getBooleanEquality() {
+ return booleanEquality;
+ }
+
+ @Config
+ @Mapping("boolean-equality")
+ public void setBooleanEquality(ConfigurationValue cv, boolean b) {
+ this.booleanEquality = b;
+ }
+
+ //
+ // 'class-name' option
+ //
+
+ private boolean className = false;
+
+ public boolean getClassName() {
+ return className;
+ }
+
+ @Config
+ @Mapping("class-name")
+ public void setClassName(ConfigurationValue cv, boolean b) {
+ this.className = b;
+ }
+
+ //
+ // 'constant-name' option
+ //
+
+ private boolean constantName = false;
+
+ public boolean getConstantName() {
+ return constantName;
+ }
+
+ @Config
+ @Mapping("constant-name")
+ public void setConstantName(ConfigurationValue cv, boolean b) {
+ this.constantName = b;
+ }
+
+ //
+ // 'constructor-dispatch-event' option
+ //
+
+ private boolean constructorDispatchEvent = false;
+
+ public boolean getConstructorDispatchEvent() {
+ return constructorDispatchEvent;
+ }
+
+ @Config
+ @Mapping("constructor-dispatch-event")
+ public void setConstructorDispatchEvent(ConfigurationValue cv, boolean b) {
+ this.constructorDispatchEvent = b;
+ }
+
+ //
+ // 'constructor-return-type' option
+ //
+
+ private boolean constructorReturnType = false;
+
+ public boolean getConstructorReturnType() {
+ return constructorReturnType;
+ }
+
+ @Config
+ @Mapping("constructor-return-type")
+ public void setConstructorReturnType(ConfigurationValue cv, boolean b) {
+ this.constructorReturnType = b;
+ }
+
+ //
+ // 'dynamic-class' option
+ //
+
+ private boolean dynamicClass = false;
+
+ public boolean getDynamicClass() {
+ return dynamicClass;
+ }
+
+ @Config
+ @Mapping("dynamic-class")
+ public void setDynamicClass(ConfigurationValue cv, boolean b) {
+ this.dynamicClass = b;
+ }
+
+ //
+ // 'empty-comment' option
+ //
+
+ private boolean emptyComment = false;
+
+ public boolean getEmptyComment() {
+ return emptyComment;
+ }
+
+ @Config
+ @Mapping("empty-comment")
+ public void setEmptyComment(ConfigurationValue cv, boolean b) {
+ this.emptyComment = b;
+ }
+
+ //
+ // 'empty-function-body' option
+ //
+
+ private boolean emptyFunctionBody = false;
+
+ public boolean getEmptyFunctionBody() {
+ return emptyFunctionBody;
+ }
+
+ @Config
+ @Mapping("empty-function-body")
+ public void setEmptyFunctionBody(ConfigurationValue cv, boolean b) {
+ this.emptyFunctionBody = b;
+ }
+
+ //
+ // 'empty-nested-block' option
+ //
+
+ private boolean emptyNestedBlock = false;
+
+ public boolean getEmptyNestedBlock() {
+ return emptyNestedBlock;
+ }
+
+ @Config
+ @Mapping("empty-nested-block")
+ public void setEmptyNestedBlock(ConfigurationValue cv, boolean b) {
+ this.emptyNestedBlock = b;
+ }
+
+ //
+ // 'empty-statement' option
+ //
+
+ private boolean emptyStatement = false;
+
+ public boolean getEmptyStatement() {
+ return emptyStatement;
+ }
+
+ @Config
+ @Mapping("empty-statement")
+ public void setEmptyStatement(ConfigurationValue cv, boolean b) {
+ this.emptyStatement = b;
+ }
+
+ //
+ // 'field-name' option
+ //
+
+ private boolean fieldName = false;
+
+ public boolean getFieldName() {
+ return fieldName;
+ }
+
+ @Config
+ @Mapping("field-name")
+ public void setFieldName(ConfigurationValue cv, boolean b) {
+ this.fieldName = b;
+ }
+
+ //
+ // 'function-name' option
+ //
+
+ private boolean functionName = false;
+
+ public boolean getFunctionName() {
+ return functionName;
+ }
+
+ @Config
+ @Mapping("function-name")
+ public void setFunctionName(ConfigurationValue cv, boolean b) {
+ this.functionName = b;
+ }
+
+ //
+ // 'if-boolean' option
+ //
+
+ private boolean ifBoolean = false;
+
+ public boolean getIfBoolean() {
+ return ifBoolean;
+ }
+
+ @Config
+ @Mapping("if-boolean")
+ public void setIfBoolean(ConfigurationValue cv, boolean b) {
+ this.ifBoolean = b;
+ }
+
+ //
+ // 'interface-name' option
+ //
+
+ private boolean interfaceName = false;
+
+ public boolean getInterfaceName() {
+ return interfaceName;
+ }
+
+ @Config
+ @Mapping("interface-name")
+ public void setInterfaceName(ConfigurationValue cv, boolean b) {
+ this.interfaceName = b;
+ }
+
+ //
+ // 'line-comment-position' option
+ //
+
+ private LineCommentPosition lineCommentPosition = null;
+
+ public String getLineCommentPosition() {
+ if (lineCommentPosition == null) {
+ return null;
+ }
+ return lineCommentPosition.getPosition();
+ }
+
+ @Config
+ @Mapping("line-comment-position")
+ public void setLineCommentPosition(ConfigurationValue cv, String s) {
+ this.lineCommentPosition = LineCommentPosition.valueOf(s.toUpperCase());
+ }
+
+ //
+ // 'local-var-param-name' option
+ //
+
+ private boolean localVarParamName = false;
+
+ public boolean getLocalVarParamName() {
+ return localVarParamName;
+ }
+
+ @Config
+ @Mapping("local-var-param-name")
+ public void setLocalVarParamName(ConfigurationValue cv, boolean b) {
+ this.localVarParamName = b;
+ }
+
+ //
+ // 'local-var-shadows-field' option
+ //
+
+ private boolean localVarShadowsField = false;
+
+ public boolean getLocalVarShadowsField() {
+ return localVarShadowsField;
+ }
+
+ @Config
+ @Mapping("local-var-shadows-field")
+ public void setLocalVarShadowsField(ConfigurationValue cv, boolean b) {
+ this.localVarShadowsField = b;
+ }
+
+ //
+ // 'max-block-depth' option
+ //
+
+ private int maxBlockDepth = 0;
+
+ public int getMaxBlockDepth() {
+ return maxBlockDepth;
+ }
+
+ @Config
+ @Mapping("max-block-depth")
+ public void setMaxBlockDepth(ConfigurationValue cv, int i) {
+ this.maxBlockDepth = i;
+ }
+
+ //
+ // 'max-params' option
+ //
+
+ private int maxParams = 0;
+
+ public int getMaxParams() {
+ return maxParams;
+ }
+
+ @Config
+ @Mapping("max-params")
+ public void setMaxParams(ConfigurationValue cv, int i) {
+ this.maxParams = i;
+ }
+
+ //
+ // 'missing-asdoc' option
+ //
+
+ private boolean missingAsdoc = false;
+
+ public boolean getMissingAsdoc() {
+ return missingAsdoc;
+ }
+
+ @Config
+ @Mapping("missing-asdoc")
+ public void setMissingAsdoc(ConfigurationValue cv, boolean b) {
+ this.missingAsdoc = b;
+ }
+
+ //
+ // 'missing-namespace' option
+ //
+
+ private boolean missingNamespace = false;
+
+ public boolean getMissingNamespace() {
+ return missingNamespace;
+ }
+
+ @Config
+ @Mapping("missing-namespace")
+ public void setMissingNamespace(ConfigurationValue cv, boolean b) {
+ this.missingNamespace = b;
+ }
+
+ //
+ // 'missing-semicolon' option
+ //
+
+ private boolean missingSemicolon = false;
+
+ public boolean getMissingSemicolon() {
+ return missingSemicolon;
+ }
+
+ @Config
+ @Mapping("missing-semicolon")
+ public void setMissingSemicolon(ConfigurationValue cv, boolean b) {
+ this.missingSemicolon = b;
+ }
+
+ //
+ // 'missing-type' option
+ //
+
+ private boolean missingType = false;
+
+ public boolean getMissingType() {
+ return missingType;
+ }
+
+ @Config
+ @Mapping("missing-type")
+ public void setMissingType(ConfigurationValue cv, boolean b) {
+ this.missingType = b;
+ }
+
+ //
+ // 'mxml-empty-attr' option
+ //
+
+ private boolean mxmlEmptyAttr = false;
+
+ public boolean getMxmlEmptyAttr() {
+ return mxmlEmptyAttr;
+ }
+
+ @Config
+ @Mapping("mxml-empty-attr")
+ public void setMxmlEmptyAttr(ConfigurationValue cv, boolean b) {
+ this.mxmlEmptyAttr = b;
+ }
+
+ //
+ // 'mxml-id' option
+ //
+
+ private boolean mxmlId = false;
+
+ public boolean getMxmlId() {
+ return mxmlId;
+ }
+
+ @Config
+ @Mapping("mxml-id")
+ public void setMxmlId(ConfigurationValue cv, boolean b) {
+ this.mxmlId = b;
+ }
+
+ //
+ // 'leading-zero' option
+ //
+
+ private boolean leadingZero = false;
+
+ public boolean getLeadingZero() {
+ return leadingZero;
+ }
+
+ @Config
+ @Mapping("leading-zero")
+ public void setLeadingZero(ConfigurationValue cv, boolean b) {
+ this.leadingZero = b;
+ }
+
+ //
+ // 'override-super' option
+ //
+
+ private boolean overrideSuper = false;
+
+ public boolean getOverrideSuper() {
+ return overrideSuper;
+ }
+
+ @Config
+ @Mapping("override-super")
+ public void setOverrideSuper(ConfigurationValue cv, boolean b) {
+ this.overrideSuper = b;
+ }
+
+ //
+ // 'package-name' option
+ //
+
+ private boolean packageName = false;
+
+ public boolean getPackageName() {
+ return packageName;
+ }
+
+ @Config
+ @Mapping("package-name")
+ public void setPackageName(ConfigurationValue cv, boolean b) {
+ this.packageName = b;
+ }
+
+ //
+ // 'static-constants' option
+ //
+
+ private boolean staticConstants = false;
+
+ public boolean getStaticConstants() {
+ return staticConstants;
+ }
+
+ @Config
+ @Mapping("static-constants")
+ public void setStaticConstants(ConfigurationValue cv, boolean b) {
+ this.staticConstants = b;
+ }
+
+ //
+ // 'strict-equality' option
+ //
+
+ private boolean strictEquality = false;
+
+ public boolean getStrictEquality() {
+ return strictEquality;
+ }
+
+ @Config
+ @Mapping("strict-equality")
+ public void setStrictEquality(ConfigurationValue cv, boolean b) {
+ this.strictEquality = b;
+ }
+
+ //
+ // 'string-event' option
+ //
+
+ private boolean stringEvent = false;
+
+ public boolean getStringEvent() {
+ return stringEvent;
+ }
+
+ @Config
+ @Mapping("string-event")
+ public void setStringEvent(ConfigurationValue cv, boolean b) {
+ this.stringEvent = b;
+ }
+
+ //
+ // 'switch-default' option
+ //
+
+ private boolean switchDefault = false;
+
+ public boolean getSwitchDefault() {
+ return switchDefault;
+ }
+
+ @Config
+ @Mapping("switch-default")
+ public void setSwitchDefault(ConfigurationValue cv, boolean b) {
+ this.switchDefault = b;
+ }
+
+ //
+ // 'this-closure' option
+ //
+
+ private boolean thisClosure = false;
+
+ public boolean getThisClosure() {
+ return thisClosure;
+ }
+
+ @Config
+ @Mapping("this-closure")
+ public void setThisClosure(ConfigurationValue cv, boolean b) {
+ this.thisClosure = b;
+ }
+
+ //
+ // 'trace' option
+ //
+
+ private boolean trace = false;
+
+ public boolean getTrace() {
+ return trace;
+ }
+
+ @Config
+ @Mapping("trace")
+ public void setTrace(ConfigurationValue cv, boolean b) {
+ this.trace = b;
+ }
+
+ //
+ // 'wildcard-import' option
+ //
+
+ private boolean wildcardImport = false;
+
+ public boolean getWildcardImport() {
+ return wildcardImport;
+ }
+
+ @Config
+ @Mapping("wildcard-import")
+ public void setWildcardImport(ConfigurationValue cv, boolean b) {
+ this.wildcardImport = b;
+ }
+
+ //
+ // 'with' option
+ //
+
+ private boolean with = false;
+
+ public boolean getWith() {
+ return with;
+ }
+
+ @Config
+ @Mapping("with")
+ public void setWith(ConfigurationValue cv, boolean b) {
+ this.with = b;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/ConfigurationBuffer.java b/linter/src/main/java/org/apache/royale/linter/config/ConfigurationBuffer.java
new file mode 100644
index 0000000..fd1dda2
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/ConfigurationBuffer.java
@@ -0,0 +1,1346 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.royale.compiler.exceptions.ConfigurationException;
+import org.apache.royale.compiler.internal.config.IConfigurationFilter;
+import org.apache.royale.compiler.internal.config.annotations.ArgumentNameGenerator;
+import org.apache.royale.compiler.internal.config.annotations.Arguments;
+import org.apache.royale.compiler.internal.config.annotations.Config;
+import org.apache.royale.compiler.internal.config.annotations.RoyaleOnly;
+import org.apache.royale.compiler.internal.config.annotations.InfiniteArguments;
+import org.apache.royale.compiler.internal.config.annotations.Mapping;
+import org.apache.royale.compiler.internal.config.annotations.SoftPrerequisites;
+import org.apache.royale.compiler.problems.ConfigurationProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.utils.Trace;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * The basic idea here is to let you keep all your configuration knowledge in
+ * your configuration object, and to automate as much as possible. Reflection is
+ * used to convert public fields and setters on your configuration object into
+ * settable vars. There are a few key concepts:
+ * <p>
+ * - You should be able to configure absolutely any object.<br>
+ * - Child configuration variables in your config become a dotted hierarchy of
+ * varnames<br>
+ * - All sources of configuration data are buffered and merged (as string
+ * var/vals) before committing to the final configuration. This class acts as
+ * the buffer.<br>
+ * - Hyphenated variables (i.e. "some-var") are automatically configured by
+ * calling your matching setter (i.e. setSomeVar)<br>
+ * - Implementing an getSomeVarInfo() method on your class lets you set up more
+ * complicated config objects<br>
+ * - You can make variables depend on other variables having been set first.
+ * This lets you set a root directory in one var and then use its value in
+ * another.<br>
+ * - Per-variable validation can be performed in setters. Overall validation
+ * should take place as a post-process step.<br>
+ * - You can keep ConfigurationBuffers around and merge multiple buffers
+ * together before committing. Most recent definitions always win.<br>
+ * <p>
+ * The contract with your configuration class:
+ * <p>
+ * - You must provide a method with the signature
+ * "void setYourVar(ConfigurationValue val)" to set your config var. Your setter
+ * method should accept either a single arg of type List or String[], or else an
+ * arglist of simple types. For example
+ * "void myvar(int a, boolean b, String c")".<br>
+ * - You can implement a function with the signature "int yourvar_argcount()" to
+ * require a different number of arguments. This limit will be enforced by
+ * configurators (command line, file, etc.)<br>
+ * - If you provide a setter and explicit parameters (i.e. not List or String[])
+ * the number of arguments will be automatically determined.<br>
+ * - Each argument to your configuration variable is assumed to have a
+ * (potentially non-unique) name. The default is the simple type of the argument
+ * (boolean, int, string). If the var takes an undetermined number of args via
+ * List or String[], the argname defaults to string.<br>
+ * - You can implement a function with the signature
+ * "String yourvar_argnames(int)" to provide names for each of the parameters.
+ * The integer passed in is the argument number. Return the same name (i.e.
+ * "item") for infinite lists.<br>
+ * - You can implement a function with the signature "String[] yourvar_deps()"
+ * to provide a list of other prerequisites for this var. You will be guaranteed
+ * that the deps are committed before your var, or else a configurationexception
+ * will be thrown if a prerequsite was unset. (Note that infinite cycles are not
+ * checked, so be careful.)<br>
+ */
+public final class ConfigurationBuffer
+{
+ public ConfigurationBuffer(Class<? extends Configuration> configClass)
+ {
+ this(configClass, new HashMap<String, String>());
+ }
+
+ public ConfigurationBuffer(Class<? extends Configuration> configClass, Map<String, String> aliases)
+ {
+ this(configClass, aliases, null);
+ }
+
+ /**
+ * Create a configuration buffer with an optional filter. The filter can be
+ * used to remove unwanted options from a super class.
+ *
+ * @param filter if null there is no filter, otherwise the set of
+ * configuration options is filtered.
+ */
+ public ConfigurationBuffer(Class<? extends Configuration> configClass, Map<String, String> aliases, IConfigurationFilter filter)
+ {
+ this.configClass = configClass;
+ this.varMap = new HashMap<String, List<ConfigurationValue>>();
+ this.committed = new HashSet<String>();
+
+ loadCache(configClass, filter);
+ assert (varCache.size() > 0) : "coding error: nothing was configurable in the provided object!";
+ for (Map.Entry<String, String> e : aliases.entrySet())
+ {
+ addAlias(e.getKey(), e.getValue());
+ }
+ }
+
+ public ConfigurationBuffer(ConfigurationBuffer copyFrom, boolean copyCommitted)
+ {
+ this.configClass = copyFrom.configClass;
+ this.varMap = new HashMap<String, List<ConfigurationValue>>(copyFrom.varMap);
+ this.committed = copyCommitted ? new HashSet<String>(copyFrom.committed) : new HashSet<String>();
+ this.varCache = copyFrom.varCache; // doesn't change after creation
+ this.varList = copyFrom.varList; // doesn't change after creation
+ this.tokens = new HashMap<String, String>(copyFrom.tokens);
+ }
+
+ public final List<String> dump()
+ {
+ final List<String> dump = new ArrayList<String>(varCache.size());
+ for (final Map.Entry<String, ConfigurationInfo> entry : varCache.entrySet())
+ {
+ dump.add(entry.getKey() + "," + entry.getValue().toString());
+ }
+ Collections.sort(dump);
+ return dump;
+ }
+
+ public void setVar(String var, String val, String source, int line) throws ConfigurationException
+ {
+ List<String> list = new LinkedList<String>();
+ list.add(val);
+ setVar(var, list, source, line, null, false);
+ }
+
+ public void setVar(String var, List<String> vals, String source, int line) throws ConfigurationException
+ {
+ setVar(var, vals, source, line, null, false);
+ }
+
+ public void setVar(String avar, List<String> vals, String source, int line, String contextPath, boolean append) throws ConfigurationException
+ {
+ String var = unalias(avar);
+ if (!isValidVar(var))
+ throw new ConfigurationException.UnknownVariable(var, source, line);
+
+ int argCount = getVarArgCount(var);
+
+ // -1 means unspecified length, its up to the receiving setter to validate.
+ if (argCount != -1)
+ {
+ addAnyDefaultArgValues(var, argCount, vals);
+
+ if (vals.size() != argCount)
+ {
+ throw new ConfigurationException.IncorrectArgumentCount(argCount, // expected
+ vals.size(), //passed
+ var, source, line);
+ }
+ }
+
+ ConfigurationValue val = new ConfigurationValue(this, var,
+ vals, //processValues( var, vals, source, line ),
+ source, line, contextPath);
+ storeValue(var, val, append);
+ committed.remove(var);
+ }
+
+ public void clearVar(String avar, String source, int line) throws ConfigurationException
+ {
+ String var = unalias(avar);
+ if (!isValidVar(var))
+ throw new ConfigurationException.UnknownVariable(var, source, line);
+ varMap.remove(var);
+ committed.remove(var);
+ }
+
+ /**
+ * Remove the configuration values came from the given source.
+ *
+ * @param source source name
+ * @see CommandLineConfigurator#SOURCE_COMMAND_LINE
+ */
+ public void clearSourceVars(String source)
+ {
+ List<String> remove = new LinkedList<String>();
+ for (Map.Entry<String, List<ConfigurationValue>> e : varMap.entrySet())
+ {
+ String var = e.getKey();
+ List<ConfigurationValue> vals = e.getValue();
+
+ List<ConfigurationValue> newvals = new LinkedList<ConfigurationValue>();
+ for (ConfigurationValue val : vals)
+ {
+ if (!val.getSource().equals(source))
+ {
+ newvals.add(val);
+ }
+ }
+ if (newvals.size() > 0)
+ varMap.put(var, newvals);
+ else
+ remove.add(var);
+ }
+ for (Iterator<String> it = remove.iterator(); it.hasNext();)
+ {
+ varMap.remove(it.next());
+ }
+ }
+
+ public List<String> processValues(String var, List<String> args, String source, int line) throws ConfigurationException
+ {
+ List<String> newArgs = new LinkedList<String>();
+ for (Iterator<String> it = args.iterator(); it.hasNext();)
+ {
+ String arg = it.next();
+
+ int depth = 100;
+ while (depth-- > 0)
+ {
+ int o = arg.indexOf("${");
+ if (o == -1)
+ break;
+
+ int c = arg.indexOf("}", o);
+
+ if (c == -1)
+ {
+ throw new ConfigurationException.Token(ConfigurationException.Token.MISSING_DELIMITER,
+ null, var, source, line);
+ }
+ String token = arg.substring(o + 2, c);
+ String value = getToken(token);
+
+ if (value == null)
+ {
+ if (value == null)
+
+ {
+ throw new ConfigurationException.Token(ConfigurationException.Token.UNKNOWN_TOKEN,
+ token, var, source, line);
+ }
+
+ }
+ arg = arg.substring(0, o) + value + arg.substring(c + 1);
+
+ }
+ if (depth == 0)
+ {
+ throw new ConfigurationException.Token(ConfigurationException.Token.RECURSION_LIMIT,
+ null, var, source, line);
+ }
+
+ newArgs.add(arg);
+ }
+ return newArgs;
+ }
+
+ public void setToken(String token, String value)
+ {
+ tokens.put(token, value);
+ }
+
+ public String getToken(String token)
+ {
+ if (tokens.containsKey(token))
+ return tokens.get(token);
+ else
+ {
+ try
+ {
+ return System.getProperty(token);
+ }
+ catch (SecurityException se)
+ {
+ return null;
+ }
+ }
+ }
+
+ private void storeValue(String avar, ConfigurationValue val, boolean append) throws ConfigurationException
+ {
+ String var = unalias(avar);
+ ConfigurationInfo info = getInfo(var);
+
+ List<ConfigurationValue> vals;
+ if (varMap.containsKey(var))
+ {
+ vals = varMap.get(var);
+ assert (vals.size() > 0);
+ ConfigurationValue first = vals.get(0);
+ if (!append && !first.getSource().equals(val.getSource()))
+ vals.clear();
+ else if (!info.allowMultiple())
+ throw new ConfigurationException.IllegalMultipleSet(
+ var,
+ val.getSource(), val.getLine());
+ }
+ else
+ {
+ vals = new LinkedList<ConfigurationValue>();
+ varMap.put(var, vals);
+ }
+ vals.add(val);
+ }
+
+ public List<ConfigurationValue> getVar(String avar)
+ {
+ String var = unalias(avar);
+ return varMap.get(var);
+ }
+
+ public Set<String> getVars()
+ {
+ return varCache.keySet();
+ }
+
+ public void merge(ConfigurationBuffer other)
+ {
+ assert (configClass == other.configClass);
+ varMap.putAll(other.varMap);
+ committed.addAll(other.committed);
+ }
+
+ private final Map<String, List<ConfigurationValue>> varMap; // list of vars that have been set
+ private final Set<String> committed; // set of vars committed to backing config
+ private final Class<? extends Configuration> configClass; // configuration class
+ private Map<String, ConfigurationInfo> varCache // info cache
+ = new HashMap<String, ConfigurationInfo>();
+ private List<String> requiredList = new LinkedList<String>(); // required vars
+ private List<String> varList = new LinkedList<String>(); // list of vars in order they should be set
+ private Map<String, String> aliases = new HashMap<String, String>(); // variable name aliases
+ private Map<String, String> tokens = new HashMap<String, String>(); // tokens for replacement
+ private List<Object[]> positions = new ArrayList<Object[]>();
+
+ private static final String SET_PREFIX = "cfg";
+ private static final String GET_PREFIX = "get";
+ private static final String INFO_SUFFIX = "Info";
+
+ //-----------------------------------------------
+ //
+
+ /**
+ * WORKAROUND FOR BUG CMP-396
+ *
+ * <p>
+ * {@link #c2h(String)} generates option names based on cfgXXX names in
+ * {@code Configuration}. Since we collapsed all the sub-configurations into
+ * one class, there's no longer a "base name" like "compiler.*" or
+ * "compiler.fonts.*". In order to preserve the dotted naming convention, we
+ * need to know which "-" separated names are actually dotted names. The
+ * {@link #CONVERT_FROM} and {@link #CONVERT_TO} is an <b>ordered</b> lookup
+ * table for option group base names. It's order makes sure that the longest
+ * possible replacement is done.
+ */
+ private static final ImmutableList<String> CONVERT_FROM =
+ ImmutableList.of(
+ "compiler-fonts-languages-",
+ "compiler-fonts-",
+ "compiler-namespaces-",
+ "compiler-mxml-",
+ "compiler-",
+ "metadata-",
+ "licenses-",
+ "frames-",
+ "runtime-shared-library-settings-");
+
+ private static final ImmutableList<String> CONVERT_TO =
+ ImmutableList.of(
+ "compiler.fonts.languages.",
+ "compiler.fonts.",
+ "compiler.namespaces.",
+ "compiler.mxml.",
+ "compiler.",
+ "metadata.",
+ "licenses.",
+ "frames.",
+ "runtime-shared-library-settings.");
+
+ /**
+ * convert StudlyCaps or camelCase to hyphenated
+ *
+ * @param camel someVar or SomeVar
+ * @return hyphen some-var
+ */
+ protected static String c2h(String camel)
+ {
+ StringBuilder b = new StringBuilder(camel.length() + 5);
+ for (int i = 0; i < camel.length(); ++i)
+ {
+ char c = camel.charAt(i);
+ if (Character.isUpperCase(c))
+ {
+ if (i != 0)
+ b.append('-');
+ b.append(Character.toLowerCase(c));
+ }
+ else
+ {
+ b.append(camel.charAt(i));
+ }
+ }
+ final String combined = b.toString();
+
+ for (int i = 0; i < CONVERT_FROM.size(); i++)
+ {
+ if (combined.startsWith(CONVERT_FROM.get(i)))
+ {
+ return combined.replaceFirst(CONVERT_FROM.get(i), CONVERT_TO.get(i));
+ }
+ }
+ return combined;
+ }
+
+ /**
+ * convert hyphenated to StudlyCaps or camelCase
+ *
+ * @param hyphenated some-var
+ * @return result
+ */
+ protected static String h2c(String hyphenated, boolean studly)
+ {
+ StringBuilder b = new StringBuilder(hyphenated.length());
+ boolean capNext = studly;
+ for (int i = 0; i < hyphenated.length(); ++i)
+ {
+ char c = hyphenated.charAt(i);
+ if (c == '-')
+ capNext = true;
+ else
+ {
+ b.append(capNext ? Character.toUpperCase(c) : c);
+ capNext = false;
+ }
+ }
+ return b.toString();
+ }
+
+ public static String varname(String membername, String basename)
+ {
+ return ((basename == null) ? membername : (basename + "." + membername));
+ }
+
+ private static ConfigurationInfo createInfo(Method setterMethod)
+ {
+ ConfigurationInfo info = null;
+
+ String infoMethodName = GET_PREFIX + setterMethod.getName().substring(SET_PREFIX.length()) + INFO_SUFFIX;
+ String getterMethodName = GET_PREFIX + setterMethod.getName().substring(SET_PREFIX.length());
+ @SuppressWarnings("unchecked")
+ Class<? extends Configuration> cfgClass = (Class<? extends Configuration>)setterMethod.getDeclaringClass();
+
+ Method infoMethod = null, getterMethod = null;
+ if (!setterMethod.isAnnotationPresent(Config.class))
+ {
+ try
+ {
+ infoMethod = cfgClass.getMethod(infoMethodName);
+
+ if (!Modifier.isStatic(infoMethod.getModifiers()))
+ {
+ assert false : ("coding error: " + cfgClass.getName() + "." + infoMethodName + " needs to be static!");
+ infoMethod = null;
+ }
+
+ info = (ConfigurationInfo)infoMethod.invoke(null, (Object[])null);
+
+ }
+ catch (SecurityException e)
+ {
+ e.printStackTrace();
+ }
+ catch (NoSuchMethodException e)
+ {
+ }
+ catch (IllegalArgumentException e)
+ {
+ e.printStackTrace();
+ }
+ catch (IllegalAccessException e)
+ {
+ e.printStackTrace();
+ }
+ catch (InvocationTargetException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ if (info == null)
+ info = new ConfigurationInfo();
+
+ try
+ {
+ getterMethod = cfgClass.getMethod(getterMethodName, (Class[])null);
+ }
+ catch (SecurityException e)
+ {
+ e.printStackTrace();
+ }
+ catch (NoSuchMethodException e)
+ {
+ }
+ info.setSetterMethod(setterMethod);
+ info.setGetterMethod(getterMethod);
+
+ return info;
+ }
+
+ /**
+ * load - prefetch all the interesting names into a dictionary so that we
+ * can find them again more easily. At the end of this call, we will have a
+ * list of every variable and their associated method.
+ *
+ * @param filter if null there is no filter, otherwise the set of
+ * configuration options is filtered.
+ */
+ private boolean loadCache(Class<? extends Configuration> cfg, IConfigurationFilter filter)
+ {
+ int count = 0;
+
+ // First, find all vars at this level.
+ for (final Method method : cfg.getMethods())
+ {
+ if (method.getName().startsWith(SET_PREFIX) ||
+ method.isAnnotationPresent(Config.class))
+ {
+ String configName = null;
+
+ final Class<?>[] pt = method.getParameterTypes();
+ assert pt.length > 1 : "Expected at least one parameters on setter.";
+
+ // Collect configuration info from getXXXInfo() static methods.
+ final ConfigurationInfo info = createInfo(method);
+
+ // Collect configuration info from annotations.
+ final Config config = method.getAnnotation(Config.class);
+ if (config != null)
+ {
+ info.isAdvanced = config.advanced();
+ info.isHidden = config.hidden();
+ info.isRemoved = config.removed();
+ info.allowMultiple = config.allowMultiple();
+ info.isPath = config.isPath();
+ info.isDisplayed = config.displayed();
+ info.isCompcOnly = config.compcOnly();
+ info.isRequired = config.isRequired();
+
+ // Argument name generator class
+ final ArgumentNameGenerator argumentNameGeneratorClass =
+ method.getAnnotation(ArgumentNameGenerator.class);
+ if (argumentNameGeneratorClass != null)
+ {
+ info.argNameGeneratorClass = argumentNameGeneratorClass.value();
+ }
+ else
+ {
+ // Argument names
+ final Arguments arguments = method.getAnnotation(Arguments.class);
+ if (arguments != null)
+ info.argnames = arguments.value();
+ }
+
+ // Argument count
+ final InfiniteArguments infinite = method.getAnnotation(InfiniteArguments.class);
+ if (infinite != null)
+ info.argcount = ConfigurationInfo.INFINITE_ARGS;
+
+ // Soft Prerequisites
+ final SoftPrerequisites softPre = method.getAnnotation(SoftPrerequisites.class);
+ if (softPre != null)
+ info.softPrerequisites = softPre.value();
+
+ // XML element name for configuration
+ final Mapping mapping = method.getAnnotation(Mapping.class);
+ if (mapping != null)
+ configName = Joiner.on(".").skipNulls().join(mapping.value());
+
+ // Is this a Flex only option?
+ final RoyaleOnly royaleOnly = method.getAnnotation(RoyaleOnly.class);
+ if (royaleOnly != null)
+ info.isRoyaleOnly = true;
+ }
+
+ // Fall back to naming convention for configuration names.
+ if (configName == null)
+ configName = c2h(method.getName().substring(SET_PREFIX.length()));
+
+ if( filter == null || filter.select(configName) )
+ {
+ varCache.put(configName, info);
+ varList.add(configName);
+ if (info.isRequired())
+ {
+ requiredList.add(configName);
+ }
+ ++count;
+ }
+ }
+ }
+
+ assert (count > 0 || filter != null) : "coding error: config class " + cfg.getName() + " did not define any setters or child configs";
+ return (count > 0);
+ }
+
+ String classToArgName(Class<?> c)
+ {
+ // we only support builtin classnames!
+
+ String className = c.getName();
+ if (className.startsWith("java.lang."))
+ className = className.substring("java.lang.".length());
+
+ return className.toLowerCase();
+ }
+
+ public ConfigurationInfo getInfo(String avar)
+ {
+ String var = unalias(avar);
+ return varCache.get(var);
+ }
+
+ public String getVarArgName(String avar, int argnum)
+ {
+ String var = unalias(avar);
+ ConfigurationInfo info = getInfo(var);
+
+ if (info == null)
+ {
+ assert false : ("must call isValid to check vars!");
+ }
+
+ return info.getArgName(argnum);
+ }
+
+ public boolean isValidVar(String avar)
+ {
+ String var = unalias(avar);
+ ConfigurationInfo info = getInfo(var);
+ return (info != null);
+ }
+
+ public int getVarArgCount(String avar)
+ {
+ ConfigurationInfo info = getInfo(avar);
+ assert (info != null);
+ return info.getArgCount();
+ }
+
+ /**
+ * Add any default values to an argument, if the user did not specify them
+ * on the command line.
+ *
+ * @param avar the argument variable
+ * @param argCount the number of argument values specified
+ * @param vals Values to add any default values to
+ */
+ private void addAnyDefaultArgValues(String avar, int argCount, List<String> vals)
+ {
+ ConfigurationInfo info = getInfo(avar);
+ final int missingArgsCount = argCount - vals.size();
+ if (missingArgsCount == 0 || info.getDefaultArgValues() == null)
+ return;
+
+ final String[] defaultArgValues = info.getDefaultArgValues();
+ final int defaultArgsCount = defaultArgValues.length;
+ final int defaultArgsStart = defaultArgsCount - missingArgsCount;
+ for (int i = defaultArgsStart; i < defaultArgsCount; i++)
+ {
+ vals.add(defaultArgValues[i]);
+ }
+ }
+
+ /**
+ * commit - bake the resolved map to the configuration
+ *
+ * @param config The configuration to set the buffer variables into.
+ * @param problems A collection where configuration problems are reported.
+ *
+ * @return true if successful, false otherwise.
+ */
+ public boolean commit(Object config, Collection<ICompilerProblem> problems)
+ {
+ assert (config.getClass() == configClass) :
+ ("coding error: configuration " + config.getClass() + " != template " + configClass);
+ Set<String> done = new HashSet<String>();
+ boolean success = true;
+
+ for (Iterator<String> vars = varList.iterator(); vars.hasNext();)
+ {
+ String var = vars.next();
+ if (varMap.containsKey(var))
+ {
+ try
+ {
+ commitVariable(config, var, done);
+ }
+ catch (ConfigurationException e)
+ {
+ problems.add(new ConfigurationProblem(e));
+ success = false;
+ }
+ }
+ }
+
+ for (Iterator<String> reqs = requiredList.iterator(); reqs.hasNext();)
+ {
+ String req = reqs.next();
+
+ if (!committed.contains(req))
+ {
+ ConfigurationException e = new ConfigurationException.MissingRequirement(req, null, null, -1);
+ problems.add(new ConfigurationProblem(
+ null,
+ -1,
+ -1,
+ -1,
+ -1,
+ e.getMessage()));
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ /**
+ * commitVariable - copy a variable out of a state into the final config.
+ * This should only be called on variables that are known to exist in the
+ * state!
+ *
+ * @param var variable name to lookup
+ * @param done set of variable names that have been completed so far (for
+ * recursion)
+ */
+ private void commitVariable(Object config, String var, Set<String> done) throws ConfigurationException
+ {
+ ConfigurationInfo info = getInfo(var);
+
+ setPrerequisites(info.getPrerequisites(), var, done, config, true);
+ setPrerequisites(info.getSoftPrerequisites(), var, done, config, false);
+
+ if (committed.contains(var))
+ return;
+
+ committed.add(var);
+ done.add(var);
+
+ assert (varMap.containsKey(var));
+ List<ConfigurationValue> vals = varMap.get(var);
+
+ if (vals.size() > 1)
+ {
+ assert (info.allowMultiple()); // assumed to have been previously checked
+ }
+ for (ConfigurationValue val : vals)
+ {
+ try
+ {
+ Object[] args = buildArgList(info, val);
+
+ info.getSetterMethod().invoke(config, args);
+ }
+ catch (Exception e)
+ {
+ Throwable t = e;
+
+ if (e instanceof InvocationTargetException)
+ {
+ t = ((InvocationTargetException)e).getTargetException();
+ }
+
+ if (Trace.error)
+ t.printStackTrace();
+
+ if (t instanceof ConfigurationException)
+ {
+ throw (ConfigurationException)t;
+ }
+ else
+ {
+ throw new ConfigurationException.OtherThrowable(t, var, val.getSource(), val.getLine());
+ }
+ }
+ }
+
+ }
+
+ private void setPrerequisites(String[] prerequisites, String var, Set<String> done, Object config, boolean required)
+ throws ConfigurationException
+ {
+ if (prerequisites != null)
+ {
+ for (int p = 0; p < prerequisites.length; ++p)
+ {
+ String depvar = prerequisites[p];
+
+ // Dependencies can only go downward.
+ int dot = var.lastIndexOf('.');
+
+ if (dot >= 0)
+ {
+ String car = var.substring(0, dot);
+ //String cdr = var.substring( dot + 1 );
+
+ String newDepvar = car + "." + depvar;
+
+ // Since in royale we have collapsed sub-configurations into one
+ // configuration, some options that were in sub-configurations now
+ // have prerequisites on options in the same configuration. We
+ // need to keep the old configuration mappings so old configurations
+ // options will still work. So a simple thing we can do is if the
+ // dependency variable is invalid (presumably because the
+ // dependency is really on a parent configuration option),
+ // then use the dependency as is depvar) and see if it is
+ // valid. If depvar ends up not being valid then set depvar
+ // to newDepvar so error reporting isn't changed by the new
+ // fall-back behavior.
+ if (isValidVar(newDepvar) || !isValidVar(depvar))
+ depvar = newDepvar;
+
+ }
+
+ if (!done.contains(depvar))
+ {
+ if (!isValidVar(depvar))
+ {
+ assert false : ("invalid " + var + " dependency " + depvar);
+ continue;
+ }
+ if (varMap.containsKey(depvar))
+ {
+ commitVariable(config, depvar, done);
+ }
+ else if (required && !committed.contains(depvar))
+ {
+ // TODO - can we get source/line for this?
+ throw new ConfigurationException.MissingRequirement(depvar, var, null, -1);
+ }
+ }
+ }
+ }
+ }
+
+ private String[] constructStringArray(List<String> args)
+ {
+ String[] sa = new String[args.size()];
+
+ int i = 0;
+ for (Iterator<String> it = args.iterator(); it.hasNext();)
+ sa[i++] = it.next();
+
+ return sa;
+ }
+
+ private Object constructValueObject(ConfigurationInfo info, ConfigurationValue cv) throws ConfigurationException
+ {
+ try
+ {
+ Class<?>[] pt = info.getSetterMethod().getParameterTypes();
+ assert (pt.length == 2); // assumed to be checked upstream
+
+ Object o = pt[1].newInstance();
+
+ Field[] fields = pt[1].getFields();
+
+ assert (fields.length == cv.getArgs().size()); // assumed to be checked upstream
+
+ Iterator<String> argsit = cv.getArgs().iterator();
+ for (int f = 0; f < fields.length; ++f)
+ {
+ String val = (String)argsit.next();
+ Object valobj = null;
+ Class<?> fc = fields[f].getType();
+
+ assert (info.getArgType(f) == fc);
+ assert (info.getArgName(f).equals(ConfigurationBuffer.c2h(fields[f].getName())));
+
+ if (fc == String.class)
+ {
+ valobj = val;
+ }
+ else if ((fc == Boolean.class) || (fc == boolean.class))
+ {
+ // TODO - Boolean.valueOf is pretty lax. Maybe we should restrict to true/false?
+ valobj = Boolean.valueOf(val);
+ }
+ else if ((fc == Integer.class) || (fc == int.class))
+ {
+ valobj = Integer.decode(val);
+ }
+ else if ((fc == Long.class) || (fc == long.class))
+ {
+ valobj = Long.decode(val);
+ }
+ else
+ {
+ assert false; // should have checked any other condition upstream!
+ }
+ fields[f].set(o, valobj);
+ }
+
+ return o;
+ }
+ catch (InstantiationException e)
+ {
+ assert false : ("coding error: unable to instantiate value object when trying to set var " + cv.getVar());
+ throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine());
+
+ }
+ catch (IllegalAccessException e)
+ {
+ assert false : ("coding error: " + e + " when trying to set var " + cv.getVar());
+ throw new ConfigurationException.OtherThrowable(e, cv.getVar(), cv.getSource(), cv.getLine());
+ }
+ }
+
+ protected static boolean isSupportedSimpleType(Class<?> c)
+ {
+ return ((c == String.class)
+ || (c == Integer.class) || (c == int.class)
+ || (c == Long.class) || (c == long.class)
+ || (c == Boolean.class) || (c == boolean.class));
+ }
+
+ protected static boolean isSupportedListType(Class<?> c)
+ {
+ return ((c == List.class) || (c == String[].class));
+ }
+
+ protected static boolean isSupportedValueType(Class<?> c)
+ {
+ if (isSupportedSimpleType(c))
+ return false;
+
+ Field[] fields = c.getFields();
+
+ for (int f = 0; f < fields.length; ++f)
+ {
+ if (!isSupportedSimpleType(fields[f].getType()))
+ return false;
+ }
+ return true;
+ }
+
+ private Object[] buildArgList(ConfigurationInfo info, ConfigurationValue val) throws ConfigurationException
+ {
+ Method setter = info.getSetterMethod();
+
+ Class<?>[] pt = setter.getParameterTypes();
+
+ List<String> args = processValues(val.getVar(), val.getArgs(), val.getSource(), val.getLine());
+
+ if (info.getArgCount() == -1)
+ {
+ if (pt.length != 2)
+ {
+ assert false : ("coding error: unlimited length setter " + val.getVar() + " must take a single argument of type List or String[]");
+ return null;
+ }
+ else if (List.class.isAssignableFrom(pt[1]))
+ {
+ return new Object[] {val, args};
+ }
+ else if (String[].class.isAssignableFrom(pt[1]))
+ {
+ return new Object[] {val, constructStringArray(args)};
+ }
+ else
+ {
+ assert false : ("coding error: unlimited length setter " + val.getVar() + " must take a single argument of type List or String[]");
+ return null;
+ }
+ }
+ else
+ {
+ assert (pt.length > 1) : ("coding error: config setter " + val.getVar() + " must accept at least one argument");
+ // ok, we first check to see if the signature of their setter accepts a list.
+
+ if (pt.length == 2)
+ {
+ // a variety of specialty setters here...
+
+ if (List.class.isAssignableFrom(pt[1]))
+ {
+ return new Object[] {val, args};
+ }
+ else if (String[].class == pt[1])
+ {
+ return new Object[] {val, constructStringArray(args)};
+ }
+ else if (isSupportedValueType(pt[1]))
+ {
+ return new Object[] {val, constructValueObject(info, val)};
+ }
+ }
+
+ // otherwise, they must have a matching size parm list as the number of args passed in.
+
+ assert (pt.length == (args.size() + 1)) : ("coding error: config setter " + val.getVar() + " does not have " + args.size() + " parameters!");
+
+ Object[] pa = new Object[pt.length];
+
+ pa[0] = val;
+
+ for (int p = 1; p < pt.length; ++p)
+ {
+ String arg = args.get(p - 1);
+ if (pt[p].isAssignableFrom(String.class))
+ {
+ pa[p] = arg;
+ }
+ else if ((pt[p] == int.class) || (pt[p] == Integer.class))
+ {
+ try
+ {
+ pa[p] = Integer.decode(arg);
+
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException.TypeMismatch(ConfigurationException.TypeMismatch.INTEGER,
+ arg, val.getVar(), val.getSource(), val.getLine());
+ }
+ }
+ else if ((pt[p] == long.class) || (pt[p] == Long.class))
+ {
+ try
+ {
+ pa[p] = Long.decode(arg);
+
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException.TypeMismatch(
+ ConfigurationException.TypeMismatch.LONG,
+ arg, val.getVar(), val.getSource(), val.getLine());
+ }
+ }
+ else if ((pt[p] == boolean.class) || (pt[p] == Boolean.class))
+ {
+ try
+ {
+ arg = arg.trim().toLowerCase();
+ if (arg.equals("true") || arg.equals("false"))
+ {
+ pa[p] = Boolean.valueOf(arg);
+ }
+ else
+ {
+ throw new ConfigurationException.TypeMismatch(
+ ConfigurationException.TypeMismatch.BOOLEAN, arg, val.getVar(), val.getSource(), val.getLine());
+ }
+ }
+ catch (Exception e)
+ {
+ throw new ConfigurationException.TypeMismatch(
+ ConfigurationException.TypeMismatch.BOOLEAN, arg, val.getVar(), val.getSource(), val.getLine());
+ }
+ }
+ else
+ {
+ assert false : ("coding error: " + val.getVar() + " setter argument " + p + " is not a supported type");
+ }
+ }
+
+ return pa;
+ }
+ }
+
+ public void addAlias(String alias, String var)
+ {
+ if (!isValidVar(var))
+ {
+ assert false : ("coding error: can't bind alias " + alias + " to nonexistent var " + var);
+ return;
+ }
+ if (aliases.containsKey(alias))
+ {
+ assert false : ("coding error: alias " + alias + " already defined as " + aliases.get(alias));
+ return;
+ }
+ if (varCache.containsKey(alias))
+ {
+ assert false : ("coding error: can't define alias " + alias + ", it already exists as a var");
+ return;
+ }
+
+ aliases.put(alias, var);
+ }
+
+ public Map<String, String> getAliases()
+ {
+ return aliases;
+ }
+
+ public String unalias(String var)
+ {
+ String realvar = aliases.get(var);
+ return (realvar == null) ? var : realvar;
+ }
+
+ public String peekSimpleConfigurationVar(String avar) throws ConfigurationException
+ {
+ String val = null;
+ List<ConfigurationValue> valList = getVar(avar);
+ if (valList != null)
+ {
+ ConfigurationValue cv = (ConfigurationValue)valList.get(0);
+ List<String> args = processValues(avar, cv.getArgs(), cv.getSource(), cv.getLine());
+ val = args.get(0);
+ }
+ return val;
+ }
+
+ public List<ConfigurationValue> peekConfigurationVar(String avar) throws ConfigurationException
+ {
+ List<ConfigurationValue> srcList = getVar(avar);
+ if (srcList == null)
+ return null;
+
+ List<ConfigurationValue> dstList = new LinkedList<ConfigurationValue>();
+ for (ConfigurationValue srcVal : srcList)
+ {
+ List<String> args = processValues(avar, srcVal.getArgs(), srcVal.getSource(), srcVal.getLine());
+
+ ConfigurationValue dstVal = new ConfigurationValue(srcVal.getBuffer(), avar, args, srcVal.getSource(), srcVal.getLine(), srcVal.getContext());
+ dstList.add(dstVal);
+ }
+ return dstList;
+ }
+
+ public void addPosition(String var, int iStart, int iEnd)
+ {
+ positions.add(new Object[] {var, new Integer(iStart), new Integer(iEnd)});
+ }
+
+ public List<Object[]> getPositions()
+ {
+ return positions;
+ }
+
+ public static List<String> formatText(String input, int columns)
+ {
+ ArrayList<String> lines = new ArrayList<String>();
+
+ if ((input == null) || (input.length() == 0))
+ return lines;
+
+ int current = 0;
+ int lineStart = -1;
+ int lineEnd = -1;
+ int wordStart = -1;
+ int wordEnd = -1;
+ boolean start = true;
+ boolean preserve = true;
+
+ while (true)
+ {
+ if (current < input.length())
+ {
+ boolean newline = input.charAt(current) == '\n';
+ boolean printable = (preserve && !newline) || !Character.isWhitespace(input.charAt(current));
+
+ if (start) // find a word
+ {
+ if (printable)
+ {
+ if (lineStart == -1)
+ {
+ lineStart = current;
+ }
+ wordStart = current;
+ start = false;
+ }
+ else
+ {
+ if (newline && lineStart != -1)
+ {
+ lines.add(input.substring(lineStart, current));
+ lineStart = -1;
+ }
+ else if (newline)
+ {
+ lines.add("");
+ }
+ ++current;
+ }
+ }
+ else
+ // have a word
+ {
+ preserve = false;
+ if (printable)
+ {
+ ++current;
+ }
+ else
+ {
+ wordEnd = current;
+ if (lineEnd == -1)
+ {
+ lineEnd = current;
+ }
+
+ // two possibilities; if the new word fits in the current line length
+ // without being too many columns, leave on current line.
+ // otherwise, set it as the start of a new line.
+
+ if (wordEnd - lineStart < columns)
+ {
+ if (newline)
+ {
+ lines.add(input.substring(lineStart, current));
+ lineStart = -1;
+ lineEnd = -1;
+ wordStart = -1;
+ start = true;
+ preserve = true;
+ ++current;
+ }
+ else
+ {
+ // we have room to add the current word to this line, find new word
+ start = true;
+ lineEnd = current;
+ }
+ }
+ else
+ {
+ // current word pushes things beyond the requested column limit,
+ // dump current text
+ lines.add(input.substring(lineStart, lineEnd));
+ lineStart = wordStart;
+ lineEnd = -1;
+ wordStart = -1;
+ start = true;
+ if (newline)
+ preserve = true;
+ }
+ }
+ }
+ }
+ else
+ // we're done
+ {
+ // a) no line yet, so don't do anything
+ // b) have line and new word would push over edge, need two lines
+ // c) have line and current word fits, need one line
+ // d) only one word and its too long anyway, need one line
+
+ if (lineStart != -1) // we have a line in progress
+ {
+ wordEnd = current;
+ if (lineEnd == -1)
+ lineEnd = current;
+
+ if (((wordEnd - lineStart) < columns) // current word fits
+ || (wordEnd == lineEnd)) // or one long word
+ {
+ lineEnd = wordEnd;
+ lines.add(input.substring(lineStart, wordEnd));
+ }
+ else
+ // didn't fit, multiple words
+ {
+ lines.add(input.substring(lineStart, lineEnd));
+ lines.add(input.substring(wordStart, wordEnd));
+ }
+ }
+ break;
+ }
+ }
+ return lines;
+ }
+
+ /**
+ * For debugging only.
+ * <p>
+ * Produces an alphabetized list of this buffer's configuration options and their values.
+ * An option such as
+ * <pre>
+ * -foo=aaa,bbb -foo+=ccc
+ * </pre>
+ * will appear as
+ * <pre>
+ * foo=aaa,bbb;ccc
+ * </pre>
+ */
+ @Override
+ public String toString()
+ {
+ StringBuffer sb = new StringBuffer();
+
+ String[] variables = varMap.keySet().toArray(new String[0]);
+ Arrays.sort(variables);
+
+ for (String var : variables)
+ {
+ sb.append(var);
+ sb.append("=");
+
+ ArrayList<String> commaSeparatedValues = new ArrayList<String>();
+ for (ConfigurationValue cv : varMap.get(var))
+ {
+ List<String> args = cv.getArgs();
+ String joinedArgs = Joiner.on(',').join(args);
+ commaSeparatedValues.add(joinedArgs);
+ }
+ String rhs = Joiner.on(';').join(commaSeparatedValues);
+ sb.append(rhs);
+
+ sb.append('\n');
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/ConfigurationInfo.java b/linter/src/main/java/org/apache/royale/linter/config/ConfigurationInfo.java
new file mode 100644
index 0000000..87588fb
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/ConfigurationInfo.java
@@ -0,0 +1,473 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.apache.royale.compiler.internal.config.annotations.DefaultArgumentValue;
+import com.google.common.base.Joiner;
+import com.google.common.base.MoreObjects;
+
+/**
+ * Meta information for each configuration options. It is created by
+ * {@link ConfigurationBuffer#loadCache} from either annotations or
+ * {@code public static ConfigurationInfo getFooInfo();} methods in
+ * {@link Configuration} class.
+ */
+public class ConfigurationInfo
+{
+ public static final int NOT_SET = -2;
+ public static final int INFINITE_ARGS = -1;
+
+ /**
+ * This ctor is used when everything can be introspected off the setter
+ * method, or else when the names/types are provided by method overrides
+ * rather than ctor arguments
+ */
+ public ConfigurationInfo()
+ {
+ this.argcount = NOT_SET;
+ this.argnames = null;
+ }
+
+ /**
+ * Simple ctor for restricting the number of arguments.
+ *
+ * @param argcount number of args, -1 for an infinite list
+ */
+ public ConfigurationInfo(int argcount)
+ {
+ this.argcount = argcount;
+ this.argnames = null;
+ }
+
+ /**
+ * Simple ctor for naming the arguments.
+ *
+ * @param argnames list of argnames, argcount will default to # of elements
+ */
+ public ConfigurationInfo(String argnames[])
+ {
+ this.argcount = argnames.length;
+ this.argnames = argnames;
+ }
+
+ /**
+ * Use this ctor when you want to set a single list of some number of
+ * identically named args
+ *
+ * @param argcount number of arguments (-1 for infinite)
+ * @param argname name of each argument
+ */
+ public ConfigurationInfo(int argcount, String argname)
+ {
+ this.argcount = argcount;
+ this.argnames = new String[] {argname};
+ }
+
+ /**
+ * More unusual ctor, this would let you have the first few args named one
+ * thing, the rest named something else. It is far more likely that you want
+ * a constrained list of names or else an arbitrary list of identical names.
+ *
+ * @param argcount number of arguments
+ * @param argnames array of argument names
+ */
+ public ConfigurationInfo(int argcount, String argnames[])
+ {
+ this.argcount = argcount;
+ this.argnames = argnames;
+ }
+
+ public final int getArgCount()
+ {
+ return argcount;
+ }
+
+ protected int argcount = NOT_SET;
+
+ protected String[] defaultArgValues = null;
+
+ /**
+ * Get any default values for an argument
+ *
+ * @return an array of default argument values. May be null
+ */
+ public final String[] getDefaultArgValues()
+ {
+ return defaultArgValues;
+ }
+
+ private static String classToArgName(Class<?> c)
+ {
+ // we only support builtin classnames!
+
+ String className = c.getName();
+ if (className.startsWith("java.lang."))
+ className = className.substring("java.lang.".length());
+
+ return className.toLowerCase();
+ }
+
+ /**
+ * Return the name of each parameter. The default implementation is usually
+ * sufficient for simple cases, but one could do wacky things here like
+ * support an infinite list of alternating arg names.
+ *
+ * @param argnum The argument number.
+ * @return name of argument
+ */
+ public String getArgName(int argnum)
+ {
+ if (argNameGeneratorClass != null)
+ {
+ Method getArgNameMethod;
+ try
+ {
+ getArgNameMethod = argNameGeneratorClass.getMethod("getArgumentName", int.class);
+ return (String)getArgNameMethod.invoke(null, argnum);
+ }
+ catch (Exception e)
+ {
+ // TODO: connect these exception to our problem logging subsystem.
+ e.printStackTrace();
+ }
+
+ return "";
+ }
+
+ if ((argnames == null) || (argnames.length == 0))
+ {
+ return classToArgName(getArgType(argnum));
+ }
+ else if (argnum >= argnames.length)
+ {
+ return argnames[argnames.length - 1];
+ }
+ else
+ {
+ return argnames[argnum];
+ }
+ }
+
+ /**
+ * Return the type of each parameter. This is computed based on your setter,
+ * and cannot be overridden
+ *
+ * @param argnum The argument number.
+ */
+ public final Class<?> getArgType(int argnum)
+ {
+ if (argnum >= argtypes.length)
+ {
+ return argtypes[argtypes.length - 1];
+ }
+ else
+ {
+ return argtypes[argnum];
+ }
+ }
+
+ protected Class<?> argNameGeneratorClass;
+ protected String[] argnames;
+ protected Class<?>[] argtypes;
+
+ protected String[] prerequisites = null;
+
+ /**
+ * Return variable names that should be set before this one. The buffer is
+ * always set such that it tries to set all variables at a given level
+ * before setting child values, but you could override by using this. Its
+ * probably a bad idea to depend on children, though. It is unnecessary to
+ * set parent vars as prerequisites, since they are implicitly set first
+ */
+ public String[] getPrerequisites()
+ {
+ return prerequisites;
+ }
+
+ protected String[] softPrerequisites = null;
+
+ /**
+ * Prerequisites which should be set before this one if they exist
+ */
+ public String[] getSoftPrerequisites()
+ {
+ return softPrerequisites;
+ }
+
+ protected boolean allowMultiple = false;
+
+ /**
+ * Variables are generally only allowed to be set once in a given
+ * file/cmdline. It is sometimes useful to allow the same set multiple times
+ * in order to aggregate values.
+ *
+ * @return true if the setter can be called multiple times
+ */
+ public boolean allowMultiple()
+ {
+ return allowMultiple;
+ }
+
+ protected String[] aliases = null;
+
+ /**
+ * Return an array of other names for this variable.
+ */
+ public String[] getAliases()
+ {
+ return aliases;
+ }
+
+ protected boolean isAdvanced = false;
+
+ /**
+ * Override to make a variable hidden by default (i.e. you need -advanced on
+ * the cmdline)
+ */
+ public boolean isAdvanced()
+ {
+ return isAdvanced;
+ }
+
+ protected boolean isHidden = false;
+
+ /**
+ * Override to make a variable completely hidden
+ */
+ public boolean isHidden()
+ {
+ return isHidden;
+ }
+
+ protected boolean isDisplayed = true;
+
+ /**
+ * Override to prevent printing when dumping configuration
+ */
+ public boolean isDisplayed()
+ {
+ return isDisplayed;
+ }
+
+ /**
+ * If a variable -must- be set, override this
+ */
+ public boolean isRequired()
+ {
+ return isRequired;
+ }
+
+ protected boolean isRequired = false;
+
+ /**
+ * Magic used by the command line configurator only at the moment to decide
+ * whether this variable should eat all subsequent arguments. Useful for
+ * -help...
+ */
+ public boolean isGreedy()
+ {
+ return isGreedy;
+ }
+
+ protected boolean isGreedy = false;
+
+ public boolean isPath()
+ {
+ return isPath;
+ }
+
+ protected boolean isPath = false;
+
+ public boolean doChecksum()
+ {
+ return true;
+ }
+
+ public String getDeprecatedMessage()
+ {
+ return deprecatedMessage;
+ }
+
+ protected String deprecatedMessage = null;
+
+ public boolean isDeprecated()
+ {
+ return isDeprecated;
+ }
+
+ protected boolean isDeprecated = false;
+
+ public String getDeprecatedReplacement()
+ {
+ return deprecatedReplacement;
+ }
+
+ protected String deprecatedReplacement;
+
+ public String getDeprecatedSince()
+ {
+ return deprecatedSince;
+ }
+
+ protected String deprecatedSince;
+
+ /**
+ * @return True indicates that the option is no longer
+ * supported and will not have any affect.
+ */
+ public boolean isRemoved()
+ {
+ return isRemoved;
+ }
+
+ protected boolean isRemoved = false;
+
+ /**
+ * @return True the option requires Flex in order to be useful.
+ */
+ public boolean isRoyaleOnly()
+ {
+ return isRoyaleOnly;
+ }
+
+ protected boolean isRoyaleOnly = false;
+
+ protected final void setSetterMethod(Method setter)
+ {
+ Class<?>[] pt = setter.getParameterTypes();
+
+ assert (pt.length >= 2) : ("coding error: config setter must take at least 2 args!");
+
+ this.setter = setter;
+
+ if (pt.length == 2)
+ {
+ Class<?> c = pt[1];
+
+ if (ConfigurationBuffer.isSupportedListType(c))
+ {
+ if (argcount == NOT_SET)
+ argcount = -1; // infinite list
+
+ argtypes = new Class[] {String.class};
+ return;
+ }
+ else if (ConfigurationBuffer.isSupportedValueType(c))
+ {
+ assert (argcount == NOT_SET) : ("coding error: value object setter cannot override argcount");
+ assert (argnames == null) : ("coding error: value object setter cannot override argnames");
+
+ Field[] fields = c.getFields();
+
+ argcount = fields.length;
+
+ assert (argcount > 0) : ("coding error: " + setter + " value object " + c.getName() + " must contain at least one public field");
+
+ argnames = new String[fields.length];
+ argtypes = new Class[fields.length];
+
+ for (int f = 0; f < fields.length; ++f)
+ {
+ argnames[f] = ConfigurationBuffer.c2h(fields[f].getName());
+ argtypes[f] = fields[f].getType();
+ }
+ return;
+ }
+ }
+
+ assert ((argcount == NOT_SET) || (argcount == pt.length - 1)) : ("coding error: the argument count must match the number of setter arguments");
+ // We've taken care of lists and value objects, from here on out, it must match the parameter list.
+
+ argcount = pt.length - 1;
+
+ DefaultArgumentValue defaultArgValuesAnno = setter.getAnnotation(DefaultArgumentValue.class);
+ if (defaultArgValuesAnno != null)
+ defaultArgValues = defaultArgValuesAnno.value();
+
+ argtypes = new Class[pt.length - 1];
+ for (int i = 1; i < pt.length; ++i)
+ {
+ assert (ConfigurationBuffer.isSupportedSimpleType(pt[i])) : ("coding error: " + setter.getClass().getName() + "." + setter.getName() + " parameter " + i + " is not a supported type!");
+ argtypes[i - 1] = pt[i];
+ }
+ }
+
+ protected final Method getSetterMethod()
+ {
+ return setter;
+ }
+
+ private Method setter;
+ private Method getter;
+
+ protected final void setGetterMethod(Method getter)
+ {
+ this.getter = getter;
+ }
+
+ protected final Method getGetterMethod()
+ {
+ return getter;
+ }
+
+ @Override
+ public String toString()
+ {
+ return MoreObjects.toStringHelper("")
+ .add("alias", arrayAsString(getAliases()))
+ .add("argcount", getArgCount())
+ .add("argnames", arrayAsString(argnames))
+ .add("argtypes", arrayAsString(argtypes))
+ .add("deprecated", isDeprecated())
+ .add("deprecatedMessage", getDeprecatedMessage())
+ .add("deprecatedReplacement", getDeprecatedReplacement())
+ .add("deprecatedSince", getDeprecatedSince())
+ .add("getter", getGetterMethod() == null ? "null" : getGetterMethod().getName())
+ .add("setter", getSetterMethod() == null ? "null" : getSetterMethod().getName())
+ .add("required", isRequired())
+ .add("Prerequisites", arrayAsString(getPrerequisites()))
+ .add("softPrerequisites", arrayAsString(getSoftPrerequisites()))
+ .add("advanced", isAdvanced())
+ .add("allow multiple", allowMultiple())
+ //.add("doChecksum", doChecksum())
+ .add("displayed", isDisplayed())
+ .add("greedy", isGreedy())
+ .add("hidden", isHidden())
+ .add("removed", isRemoved())
+ .add("path", isPath())
+ .toString();
+ }
+
+ private String arrayAsString(Object[] array)
+ {
+ if (array == null)
+ return "";
+ else
+ return "[" + Joiner.on(",").join(array) + "]";
+ }
+
+ /**
+ * True if only {@code compc} client can use this option.
+ */
+ public boolean isCompcOnly = false;
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/ConfigurationValue.java b/linter/src/main/java/org/apache/royale/linter/config/ConfigurationValue.java
new file mode 100644
index 0000000..5eed4f5
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/ConfigurationValue.java
@@ -0,0 +1,109 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.util.List;
+import java.util.LinkedList;
+
+
+/**
+ * This class represents an instance of a configuration option. For
+ * example, "-debug=true".
+ */
+public class ConfigurationValue
+{
+ protected ConfigurationValue( ConfigurationBuffer buffer, String var, List<String> args, String source, int line, String context )
+ {
+ this.buffer = buffer;
+ this.var = var;
+ this.args = new LinkedList<String>( args );
+ this.source = source;
+ this.line = line;
+ this.context = context;
+ }
+
+ /**
+ * getArgs
+ *
+ * @return list of values provided, in schema order
+ */
+ public final List<String> getArgs()
+ {
+ return args;
+ }
+
+ /**
+ * getBuffer
+ *
+ * @return a handle to the associated buffer holding this value
+ */
+ public final ConfigurationBuffer getBuffer()
+ {
+ return buffer;
+ }
+
+ /**
+ * getSource
+ *
+ * @return a string representing the origin of this value, or null if unknown
+ */
+ public final String getSource()
+ {
+ return source;
+ }
+
+ /**
+ * getLine
+ *
+ * @return the line number of the origin of this value, or -1 if unknown
+ */
+ public final int getLine()
+ {
+ return line;
+ }
+
+ /**
+ * getVar
+ *
+ * @return the full name of this configuration variable in the hierarchy
+ */
+ public final String getVar()
+ {
+ return var;
+ }
+
+ /**
+ * getContext
+ *
+ * @return the path of the enclosing context where the variable was set
+ * (i.e. the directory where the config file was found)
+ */
+ public final String getContext()
+ {
+ return context;
+ }
+
+ private final ConfigurationBuffer buffer;
+ private final String var;
+ private final List<String> args;
+ private final String source;
+ private final int line;
+ private final String context;
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/Configurator.java b/linter/src/main/java/org/apache/royale/linter/config/Configurator.java
new file mode 100644
index 0000000..7af4f1a
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/Configurator.java
@@ -0,0 +1,683 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.royale.compiler.exceptions.ConfigurationException;
+import org.apache.royale.compiler.internal.config.localization.LocalizationManager;
+import org.apache.royale.compiler.internal.config.localization.ResourceBundleLocalizer;
+import org.apache.royale.compiler.problems.ConfigurationProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.utils.FilenameNormalization;
+import org.apache.royale.utils.Trace;
+
+/**
+ * A class that allows a client change compiler settings and to
+ * configure projects and targets from those settings.
+ */
+public class Configurator implements Cloneable
+{
+ /**
+ * Convert file path strings to {@code File} objects. Null values are
+ * discarded.
+ *
+ * @param paths List of file paths
+ * @return List of File objects. No null values will be returned.
+ */
+ public static List<File> toFiles(final List<String> paths)
+ {
+ final List<File> result = new ArrayList<File>();
+ for (final String path : paths)
+ {
+ if (path != null)
+ result.add(new File(path));
+ }
+ return result;
+ }
+
+ /**
+ * Convert file path strings to {@code File} objects. Null values are
+ * discarded.
+ *
+ * @param paths List of file paths
+ * @return Array of File objects. No null values will be returned.
+ */
+ public static List<File> toFileList(final List<String> paths)
+ {
+ final List<File> result = new ArrayList<File>();
+ for (final String path : paths)
+ {
+ if (path != null)
+ result.add(FilenameNormalization.normalize(new File(path)));
+ }
+ return result;
+ }
+
+ /**
+ * Convert {@code File} objects to {@code String}, where each {@code String} is
+ * the absolute file path of the file. Null values are discarded.
+ *
+ * @param files file specifications
+ * @return Array of File objects. No null values will be returned.
+ */
+ public static String[] toPaths(File[] files)
+ {
+ final List<String> result = new ArrayList<String>();
+ for (final File file : files)
+ {
+ if (file != null)
+ result.add(file.getAbsolutePath());
+ }
+ return result.toArray(new String[0]);
+ }
+
+ // Used to generate the command line
+ private static final String EQUALS_STRING = "=";
+ private static final String PLUS_EQUALS_STRING = "+=";
+ private static final String COMMA_STRING = ",";
+ private static final String PLUS_STRING = "+";
+ /**
+ * Constructor
+ */
+ public Configurator()
+ {
+ this(Configuration.class);
+ }
+
+ /**
+ * Constructor
+ */
+ public Configurator(Class<? extends Configuration> configurationClass)
+ {
+ this.configurationClass = configurationClass;
+ args = new LinkedHashMap<String, Object>();
+ more = new LinkedHashMap<String, Object>();
+ tokens = new TreeMap<String, String>();
+
+ isConfigurationDirty = true;
+ configurationDefaultVariable = ILinterSettingsConstants.FILES; // the default variable of the configuration.
+ configurationProblems = new ArrayList<ICompilerProblem>();
+
+ // initialize the localization manager.
+ LocalizationManager.get().addLocalizer(new ResourceBundleLocalizer());
+ }
+
+ private ConfigurationBuffer cfgbuf;
+ protected Configuration configuration;
+ private Class<? extends Configuration> configurationClass;
+
+ private Map<String, Object> args, more;
+ private String[] extras;
+ private String configurationDefaultVariable;
+
+ private Map<String, String> tokens;
+
+ private boolean isConfigurationDirty;
+ private boolean configurationSuccess;
+ protected Collection<ICompilerProblem> configurationProblems;
+
+ //
+ // IConfigurator related methods
+ //
+
+ public Collection<ICompilerProblem> validateConfiguration(String[] args)
+ {
+ if (args == null)
+ throw new NullPointerException("args may not be null");
+
+ List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>();
+ ConfigurationBuffer configurationBuffer = createConfigurationBuffer(configurationClass);
+
+ try
+ {
+ CommandLineConfigurator.parse(configurationBuffer, null, args);
+ }
+ catch (ConfigurationException e)
+ {
+ final ICompilerProblem problem = new ConfigurationProblem(e);
+ problems.add(problem);
+ }
+
+ return problems;
+ }
+
+ public Collection<ICompilerProblem> getConfigurationProblems()
+ {
+ assert configuration != null :
+ "Get the configuration problems after calling getConfiguration()";
+
+ List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(configurationProblems.size() +
+ configuration.getConfigurationProblems().size());
+ problems.addAll(configurationProblems);
+ problems.addAll(configuration.getConfigurationProblems());
+ return problems;
+ }
+
+ public Configuration getConfiguration()
+ {
+ processConfiguration();
+ return configuration;
+ }
+
+ public ConfigurationBuffer getConfigurationBuffer()
+ {
+ return cfgbuf;
+ }
+
+ /**
+ * Create a new configuration instance. The Configurator will need to
+ * create a new configuration for each new configuration. For example,
+ * creating a new Configurator and getting the target settings will create
+ * a new configuration. If later on, the configuration is modified by calling
+ * any of the setter methods on the Configurator, then a new configuration
+ * will be created the next time applyToProject() or getTargetSettings() is called.
+ *
+ * The method may be overriden to allow for greater control when creating a
+ * custom configuration that extends the built-in configuration.
+ *
+ * @return a new configuration instance. If the custom configuration class
+ * cannot be created, the default configuration class will be created instead.
+ */
+ protected Configuration createConfiguration()
+ {
+ try
+ {
+ return configurationClass.newInstance();
+ }
+ catch (Exception e)
+ {
+ // If there is a problem initializing the configuration, then
+ // throw a ConfigurationException.
+ reportConfigurationException(new ConfigurationException.CouldNotInstantiate(configurationClass.getName()));
+
+ // Create the default configuration so we can report configuration
+ // problems.
+ try
+ {
+ return Configuration.class.newInstance();
+ }
+ catch (Exception e2)
+ {
+ // this should never fail
+ assert(false);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Initialize the configuration and the configuration buffer.
+ */
+ protected void initializeConfiguration()
+ {
+ // Create a clean configuration and configuration buffer
+ configuration = createConfiguration();
+ cfgbuf = createConfigurationBuffer(configuration.getClass());
+ }
+
+ /**
+ * Create a configuration buffer.
+ * @param configurationClass The Configuration object
+ * @return the configuration buffer to use
+ */
+ protected ConfigurationBuffer createConfigurationBuffer(
+ Class<? extends Configuration> configurationClass)
+ {
+ return new ConfigurationBuffer(
+ configurationClass, Configuration.getAliases());
+ }
+
+ /**
+ * Wrapper around the real processConfiguration.
+ *
+ * @return true if success, false otherwise.
+ */
+ protected boolean processConfiguration()
+ {
+ boolean success = true;
+
+ if (isConfigurationDirty)
+ {
+ configurationProblems.clear();
+
+ try
+ {
+ success = processConfiguration(getOptions(args, more, processExtras(extras)));
+ }
+ catch (ConfigurationException e)
+ {
+ reportConfigurationException(e);
+ success = false;
+ }
+ catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ else
+ {
+ success = configurationSuccess;
+ }
+
+ isConfigurationDirty = false;
+ configurationSuccess = success;
+ return success;
+ }
+
+ /**
+ * Does all the work to set the command line arguments info the
+ * configuration object.
+ *
+ * @param argsArray - command line arguments
+ *
+ * @return true if successful, false otherwise.
+ */
+ protected boolean processConfiguration(String[] argsArray)
+ {
+ initializeConfiguration();
+
+ boolean success = true;
+
+ try
+ {
+ SystemPropertyConfigurator.load(cfgbuf, "royale");
+
+ // Parse the command line a first time, to peak at stuff like
+ // "royalelib" and "load-config". The first parse is thrown
+ // away after that and we intentionally parse a second time
+ // below. See note below.
+ CommandLineConfigurator.parse(cfgbuf, configurationDefaultVariable, argsArray);
+
+ overrideDefaults();
+
+ // Return if "-version" is present so the command line can print the
+ // version.
+ if (cfgbuf.getVar("version") != null)
+ return false;
+
+ // Return so the command line can print help if "-help" is present.
+ final List<ConfigurationValue> helpVar = cfgbuf.getVar("help");
+ if (helpVar != null)
+ return false;
+
+ // The command line needs to take precedence over all defaults and config files.
+ // By simply re-merging the command line back on top,
+ // we will get the behavior we want.
+ cfgbuf.clearSourceVars(CommandLineConfigurator.source);
+ CommandLineConfigurator.parse(cfgbuf, configurationDefaultVariable, argsArray);
+
+ // commit() reports problems instead of throwing an exception. This
+ // allows us to process all the options in a configuration that
+ // are correct in the hopes that it will be enough to configure a
+ // project.
+ if (!cfgbuf.commit(configuration, configurationProblems))
+ success = false;
+
+ configuration.validate(cfgbuf);
+ }
+ catch (ConfigurationException e)
+ {
+ reportConfigurationException(e);
+ success = false;
+ }
+
+ return success;
+ }
+
+ /**
+ * Override default values.
+ */
+ protected void overrideDefaults() throws ConfigurationException
+ {
+ }
+
+ /**
+ * Convert conifguration exceptions to problems and collect them for
+ * reporting.
+ *
+ * @param e
+ */
+ protected void reportConfigurationException(ConfigurationException e)
+ {
+ final ICompilerProblem problem = new ConfigurationProblem(e);
+ configurationProblems.add(problem);
+ }
+
+ //
+ // Configuration related methods
+ //
+ protected String[] getOptions(Map<String, Object> args, Map<String, Object> more,
+ String[] extras)
+ {
+ ArrayList<String> buffer = new ArrayList<String>();
+
+ for (Map.Entry<String, String> tokenEntry : tokens.entrySet())
+ {
+ buffer.add(PLUS_STRING + tokenEntry.getKey() + EQUALS_STRING + tokenEntry.getValue());
+ }
+
+ for (Map.Entry<String, Object> arg : args.entrySet())
+ {
+ String key = arg.getKey();
+ Object value = arg.getValue();
+
+ if (value instanceof Boolean)
+ {
+ buffer.add(key + EQUALS_STRING + value);
+ }
+ else if (value instanceof Number)
+ {
+ buffer.add(key);
+ buffer.add(value.toString());
+ }
+ else if (value instanceof String)
+ {
+ if (!"".equals(value))
+ {
+ buffer.add(key);
+ buffer.add((String)value);
+ }
+ else
+ {
+ buffer.add(key + EQUALS_STRING);
+ }
+ }
+ else if (value instanceof File)
+ {
+ String p = ((File) value).getPath();
+ if (!"".equals(p))
+ {
+ buffer.add(key);
+ buffer.add(p);
+ }
+ else
+ {
+ buffer.add(key + EQUALS_STRING);
+ }
+ }
+ else if (value instanceof java.util.Date)
+ {
+ buffer.add(key);
+ buffer.add(value.toString());
+ }
+ else if (value instanceof Map)
+ {
+ @SuppressWarnings("unchecked")
+ Map<String, ?> m = (Map<String, ?>) value;
+ for (Map.Entry<String, ?>entry : m.entrySet())
+ {
+ String k = entry.getKey();
+ Object v = entry.getValue();
+
+ if (v instanceof String)
+ {
+ buffer.add(key);
+ buffer.add(k);
+ buffer.add((String)v);
+ }
+ else if (v instanceof File)
+ {
+ buffer.add(key);
+ buffer.add(k);
+ buffer.add(((File) v).getPath());
+ }
+ else if (v instanceof Collection)
+ {
+ buffer.add(key);
+ buffer.add(k);
+ Collection<?> list = (Collection<?>)v;
+ for (Object next : list)
+ {
+ if (next != null)
+ buffer.add(next.toString());
+ }
+ }
+ else if (v != null)
+ {
+ assert false;
+ }
+ }
+ }
+ else if (value instanceof int[])
+ {
+ int[] a = (int[]) value;
+ buffer.add(key);
+ buffer.add(String.valueOf(a[0]));
+ buffer.add(String.valueOf(a[1]));
+ }
+ else if (value instanceof Collection)
+ {
+ Collection<Object> list = new LinkedList<Object>((Collection<?>)args.get(key));
+
+ int length = list.size();
+ if (length > 0)
+ {
+ buffer.add(key);
+ }
+ for (Object obj : list)
+ {
+ if (obj instanceof String)
+ {
+ buffer.add((String)obj);
+ }
+ else if (obj instanceof File)
+ {
+ buffer.add(((File)obj).getPath());
+ }
+ }
+ }
+ else if (value != null)
+ {
+ assert false;
+ }
+ else
+ {
+ // System.err.println("unprocessed compiler options: " + key + EQUALS_STRING + value);
+ }
+ }
+
+ for (Map.Entry<String, Object> moreEntry : more.entrySet())
+ {
+ String key = moreEntry.getKey();
+ Object value = moreEntry.getValue();
+
+ if (value instanceof Collection)
+ {
+ buffer.add(key + PLUS_EQUALS_STRING + toCommaSeparatedString((Collection<?>)value));
+ }
+ else if (value instanceof Map)
+ {
+ @SuppressWarnings("unchecked")
+ Map<String, ?> m = (Map<String, ?>) value;
+ for (Map.Entry<String, ?>entry : m.entrySet())
+ {
+ String k = entry.getKey();
+ Object v = entry.getValue();
+
+ if (v instanceof Collection)
+ {
+ buffer.add(key + PLUS_EQUALS_STRING + k + COMMA_STRING +
+ toCommaSeparatedString((Collection<?>)v));
+ }
+ else if (v != null)
+ {
+ assert false;
+ }
+ }
+ }
+ else if (value != null)
+ {
+ assert false;
+ }
+ else
+ {
+ // System.err.println("unprocessed compiler options: " + key + EQUALS_STRING + value);
+ }
+ }
+
+ // Append extra command line args to the buffer.
+ if (extras != null && extras.length > 0)
+ {
+ for (int i = 0, length = extras == null ? 0 : extras.length; i < length; i++)
+ {
+ if (extras[i] != null)
+ {
+ buffer.add(extras[i]);
+ }
+ }
+ }
+
+ String[] options = new String[buffer.size()];
+ buffer.toArray(options);
+
+ if (Trace.config)
+ Trace.trace("Configurator: options = " + buffer.toString());
+
+ return options;
+ }
+
+ protected String[] processExtras(String[] extraOptions) throws ConfigurationException
+ {
+ return extraOptions;
+ }
+
+ /**
+ * Sets the configuration parameters. The input should be valid <code>mxmlc/compc</code> command-line arguments.<p>
+ *
+ * @param args <code>mxmlc/compc</code> command-line arguments
+ * @param defaultVariable the default variable of the configuration.
+ */
+ public void setConfiguration(String[] args, String defaultVariable)
+ {
+ extras = args;
+ configurationDefaultVariable = defaultVariable;
+ isConfigurationDirty = true;
+ }
+
+ /**
+ * Defines a token. mxmlc and compc support token substitutions. For example,
+ *
+ * <pre>
+ * mxmlc +royalelib=path1 +foo=bar --var=${foo}
+ * </pre>
+ *
+ * <code>var=bar</code> after the substitution of <code>${foo}</code>.
+ *
+ * @param name The name of the token.
+ * @param value The value of the token.
+ */
+ public void setToken(String name, String value)
+ {
+ tokens.put(name, value);
+
+ isConfigurationDirty = true;
+ }
+
+ /**
+ *
+ */
+ @Override
+ public String toString()
+ {
+ String[] options;
+ try
+ {
+ options = getOptions(args, more, processExtras(extras));
+ }
+ catch (ConfigurationException e)
+ {
+ options = new String[0];
+ }
+
+ StringBuilder b = new StringBuilder();
+ for (int i = 0; i < options.length; i++)
+ {
+ b.append(options[i]);
+ b.append(' ');
+ }
+ return b.toString();
+ }
+
+ private String toCommaSeparatedString(Collection<?> values)
+ {
+ StringBuilder b = new StringBuilder();
+ int length = values.size();
+ int i = 0;
+ for (Object value : values)
+ {
+ String valueString = null;
+
+ if (value instanceof String)
+ {
+ valueString = (String)value;
+ }
+ else if (value instanceof File)
+ {
+ valueString = ((File)value).getPath();
+ }
+
+ if (valueString != null)
+ b.append(valueString);
+
+ if (i++ < length - 1)
+ {
+ b.append(COMMA_STRING);
+ }
+ }
+ return b.toString();
+ }
+
+ @Override
+ public Configurator clone()
+ {
+ Configurator cloneConfig;
+
+ try
+ {
+ cloneConfig = (Configurator) super.clone();
+ }
+ catch ( CloneNotSupportedException e )
+ {
+ throw new RuntimeException(e);//wont happen
+ }
+
+ cloneConfig.args = new LinkedHashMap<String, Object>(args);
+ cloneConfig.more = new LinkedHashMap<String, Object>(more);
+ cloneConfig.tokens = new LinkedHashMap<String, String>(tokens);
+ cloneConfig.configurationClass = configurationClass;
+ cloneConfig.isConfigurationDirty = true;
+
+ if (extras != null)
+ {
+ cloneConfig.extras = new String[extras.length];
+ System.arraycopy(extras, 0, cloneConfig.extras, 0, extras.length);
+ }
+
+ return cloneConfig;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/ILinterSettingsConstants.java b/linter/src/main/java/org/apache/royale/linter/config/ILinterSettingsConstants.java
new file mode 100644
index 0000000..9fb238f
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/ILinterSettingsConstants.java
@@ -0,0 +1,24 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+public interface ILinterSettingsConstants {
+ static final String FILES = "files";
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/LineCommentPosition.java b/linter/src/main/java/org/apache/royale/linter/config/LineCommentPosition.java
new file mode 100644
index 0000000..26b90f2
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/LineCommentPosition.java
@@ -0,0 +1,36 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+public enum LineCommentPosition {
+ ABOVE("above"),
+ BESIDE("beside");
+
+ private LineCommentPosition(String position) {
+ this.position = position;
+ }
+
+ private String position;
+
+ public String getPosition() {
+ return position;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/config/SystemPropertyConfigurator.java b/linter/src/main/java/org/apache/royale/linter/config/SystemPropertyConfigurator.java
new file mode 100644
index 0000000..f1698da
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/config/SystemPropertyConfigurator.java
@@ -0,0 +1,82 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.royale.linter.config;
+
+import java.util.Properties;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.LinkedList;
+import java.util.StringTokenizer;
+
+import org.apache.royale.compiler.exceptions.ConfigurationException;
+
+/**
+ * A utility class, which is used to load configuration options via
+ * system properties and populate a ConfigurationBuffer. A
+ * counterpart of CommandLineConfigurator and FileConfigurator.
+ */
+public class SystemPropertyConfigurator
+{
+ /**
+ * Opportunistically find some configuration settings in system properties.
+ * @param buffer the intermediate config buffer
+ * @param prefix an optional prefix to add to the variable, pass null if no prefix
+ */
+ public static void load( final ConfigurationBuffer buffer, String prefix ) throws ConfigurationException
+ {
+ try
+ {
+ Properties props = System.getProperties();
+
+ for (Enumeration<?> e = props.propertyNames(); e.hasMoreElements();)
+ {
+ String propname = (String) e.nextElement();
+
+ if (!propname.startsWith( prefix + "."))
+ {
+ String value = System.getProperty( propname );
+ buffer.setToken( propname, value );
+ continue;
+ }
+
+ String varname = propname.substring( prefix.length() + 1 );
+
+ if (!buffer.isValidVar( varname ))
+ continue;
+
+ String value = System.getProperty( propname );
+
+ List<String> args = new LinkedList<String>();
+ StringTokenizer t = new StringTokenizer( value, "," );
+
+ while (t.hasMoreTokens())
+ {
+ String token = t.nextToken();
+ args.add( token );
+ }
+ buffer.setVar( varname, args, "system properties", -1 );
+ }
+ }
+ catch (SecurityException se)
+ {
+ // just ignore, this is an optional for loading configuration
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/internal/BaseLinter.java b/linter/src/main/java/org/apache/royale/linter/internal/BaseLinter.java
new file mode 100644
index 0000000..d825e53
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/internal/BaseLinter.java
@@ -0,0 +1,46 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.internal;
+
+import java.util.Collection;
+
+import org.apache.royale.compiler.clients.problems.CompilerProblemCategorizer;
+import org.apache.royale.compiler.problems.CompilerProblemSeverity;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterSettings;
+
+public class BaseLinter {
+ protected LinterSettings settings;
+
+ protected BaseLinter(LinterSettings settings) {
+ this.settings = settings;
+ }
+
+ protected boolean hasErrors(Collection<ICompilerProblem> problems) {
+ CompilerProblemCategorizer categorizer = new CompilerProblemCategorizer(null);
+ for (ICompilerProblem problem : problems) {
+ CompilerProblemSeverity severity = categorizer.getProblemSeverity(problem);
+ if (CompilerProblemSeverity.ERROR.equals(severity)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/AnyTypeRule.java b/linter/src/main/java/org/apache/royale/linter/rules/AnyTypeRule.java
new file mode 100644
index 0000000..3215ec9
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/AnyTypeRule.java
@@ -0,0 +1,127 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.constants.IASLanguageConstants;
+import org.apache.royale.compiler.internal.tree.as.IdentifierNode;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IIdentifierNode;
+import org.apache.royale.compiler.tree.as.IParameterNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for uses of the * type.
+ */
+public class AnyTypeRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IExpressionNode typeNode = variableNode.getVariableTypeNode();
+ if (!isAnyType(typeNode)) {
+ return;
+ }
+ problems.add(new AnyTypeOnVariableLinterProblem(variableNode));
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ for (IParameterNode paramNode : functionNode.getParameterNodes()) {
+ IExpressionNode typeNode = paramNode.getVariableTypeNode();
+ if (isAnyType(typeNode)) {
+ problems.add(new AnyTypeOnParameterLinterProblem(paramNode));
+ }
+ }
+ if (functionNode.isConstructor()) {
+ return;
+ }
+ IExpressionNode typeNode = functionNode.getReturnTypeNode();
+ if (isAnyType(typeNode)) {
+ problems.add(new AnyTypeReturnLinterProblem(functionNode));
+ }
+ }
+
+ private boolean isAnyType(IExpressionNode typeNode) {
+ if (!(typeNode instanceof IIdentifierNode)) {
+ return false;
+ }
+ // isImplicit() is not on the interface, for some reason
+ if (typeNode instanceof IdentifierNode && ((IdentifierNode) typeNode).isImplicit()) {
+ return false;
+ }
+ IIdentifierNode identifierNode = (IIdentifierNode) typeNode;
+ if (!IASLanguageConstants.ANY_TYPE.equals(identifierNode.getName())) {
+ return false;
+ }
+ return true;
+ }
+
+ public static class AnyTypeOnVariableLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must not use the * type for variable '${varName}'";
+
+ public AnyTypeOnVariableLinterProblem(IVariableNode node)
+ {
+ super(node.getVariableTypeNode());
+ varName = node.getName();
+ }
+
+ public String varName;
+ }
+
+ public static class AnyTypeOnParameterLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must not use the * type for function parameter '${paramName}'";
+
+ public AnyTypeOnParameterLinterProblem(IParameterNode node)
+ {
+ super(node.getVariableTypeNode());
+ paramName = node.getName();
+ }
+
+ public String paramName;
+ }
+
+ public static class AnyTypeReturnLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must not use the * type for function return type";
+
+ public AnyTypeReturnLinterProblem(IFunctionNode node)
+ {
+ super(node.getReturnTypeNode());
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/BooleanEqualityRule.java b/linter/src/main/java/org/apache/royale/linter/rules/BooleanEqualityRule.java
new file mode 100644
index 0000000..0aba1e4
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/BooleanEqualityRule.java
@@ -0,0 +1,75 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IBinaryOperatorNode;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for redundant equality comparisons with 'true' and 'false' boolean
+ * literals using the '==' and '!=' operators.
+ *
+ * Does not check for strict equality using the '===' and '!==' operators
+ * because these operators do not type coerce the two sides to determine if they
+ * are 'truthy' or 'falsy'.
+ */
+public class BooleanEqualityRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.Op_EqualID, (node, tokenQuery, problems) -> {
+ checkBinaryOperatorNode((IBinaryOperatorNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.Op_NotEqualID, (node, tokenQuery, problems) -> {
+ checkBinaryOperatorNode((IBinaryOperatorNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkBinaryOperatorNode(IBinaryOperatorNode operatorNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IExpressionNode leftOperandNode = operatorNode.getLeftOperandNode();
+ if (ASTNodeID.LiteralBooleanID.equals(leftOperandNode.getNodeID())) {
+ problems.add(new BooleanEqualityLinterProblem(leftOperandNode));
+ }
+ IExpressionNode rightOperandNode = operatorNode.getRightOperandNode();
+ if (ASTNodeID.LiteralBooleanID.equals(rightOperandNode.getNodeID())) {
+ problems.add(new BooleanEqualityLinterProblem(rightOperandNode));
+ }
+ }
+
+ public static class BooleanEqualityLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must simplify statement to remove redundant comparison with true or false";
+
+ public BooleanEqualityLinterProblem(IExpressionNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/ClassNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/ClassNameRule.java
new file mode 100644
index 0000000..462f28a
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/ClassNameRule.java
@@ -0,0 +1,77 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IClassNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that class names match a specific pattern.
+ */
+public class ClassNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[A-Z][a-zA-Z0-9]*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.ClassID, (node, tokenQuery, problems) -> {
+ checkClassNode((IClassNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkClassNode(IClassNode classNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String className = classNode.getName();
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(className);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new ClassNameLinterProblem(classNode, thePattern));
+ }
+
+ public static class ClassNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Class name does not match the pattern '${pattern}'";
+
+ public ClassNameLinterProblem(IClassNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ }
+
+ public String pattern;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/ConstantNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/ConstantNameRule.java
new file mode 100644
index 0000000..16f8b1c
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/ConstantNameRule.java
@@ -0,0 +1,80 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that constant names match a specific pattern.
+ */
+public class ConstantNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!variableNode.isConst()) {
+ return;
+ }
+ String constantName = variableNode.getName();
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(constantName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new ConstantNameLinterProblem(variableNode, thePattern));
+ }
+
+ public static class ConstantNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Constant name does not match the pattern '${pattern}'";
+
+ public ConstantNameLinterProblem(IVariableNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ }
+
+ public String pattern;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/ConstructorDispatchEventRule.java b/linter/src/main/java/org/apache/royale/linter/rules/ConstructorDispatchEventRule.java
new file mode 100644
index 0000000..736ebb4
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/ConstructorDispatchEventRule.java
@@ -0,0 +1,97 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IFunctionCallNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IIdentifierNode;
+import org.apache.royale.compiler.tree.as.ILanguageIdentifierNode;
+import org.apache.royale.compiler.tree.as.IMemberAccessExpressionNode;
+import org.apache.royale.compiler.tree.as.ILanguageIdentifierNode.LanguageIdentifierKind;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that a constructor does not call `dispatchEvent` because it's likely
+ * that no listeners have been added yet.
+ */
+public class ConstructorDispatchEventRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionCallID, (node, tokenQuery, problems) -> {
+ checkFunctionCallNode((IFunctionCallNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionCallNode(IFunctionCallNode functionCallNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IFunctionNode functionNode = (IFunctionNode) functionCallNode.getAncestorOfType(IFunctionNode.class);
+ if (functionNode == null || !functionNode.isConstructor()) {
+ return;
+ }
+ IExpressionNode nameNode = functionCallNode.getNameNode();
+ if (nameNode instanceof IIdentifierNode) {
+ IIdentifierNode identifierNode = (IIdentifierNode) nameNode;
+ if ("dispatchEvent".equals(identifierNode.getName())) {
+ problems.add(new ConstructorDispatchEventLinterProblem(functionNode, identifierNode));
+ return;
+ }
+ return;
+ }
+ if (nameNode instanceof IMemberAccessExpressionNode) {
+ IMemberAccessExpressionNode memberAccess = (IMemberAccessExpressionNode) nameNode;
+ if (memberAccess.getRightOperandNode() instanceof IIdentifierNode) {
+ IIdentifierNode identifierNode = (IIdentifierNode) memberAccess.getRightOperandNode();
+ if ("dispatchEvent".equals(identifierNode.getName())) {
+ if (memberAccess.getLeftOperandNode() instanceof ILanguageIdentifierNode) {
+ ILanguageIdentifierNode langIdentifierNode = (ILanguageIdentifierNode) memberAccess.getLeftOperandNode();
+ if (LanguageIdentifierKind.THIS.equals(langIdentifierNode.getKind())
+ || LanguageIdentifierKind.SUPER.equals(langIdentifierNode.getKind())) {
+ problems.add(new ConstructorDispatchEventLinterProblem(functionNode, identifierNode));
+ return;
+ }
+ }
+ }
+ }
+ return;
+ }
+ }
+
+ public static class ConstructorDispatchEventLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Constructor '${functionName}' must not call 'dispatchEvent'";
+
+ public ConstructorDispatchEventLinterProblem(IFunctionNode functionNode, IExpressionNode dispatchEventNode) {
+ super(dispatchEventNode);
+ functionName = functionNode.getName();
+ }
+
+ public String functionName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/ConstructorReturnTypeRule.java b/linter/src/main/java/org/apache/royale/linter/rules/ConstructorReturnTypeRule.java
new file mode 100644
index 0000000..2dc8700
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/ConstructorReturnTypeRule.java
@@ -0,0 +1,72 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that a constructor does not specify a return type (not even `void`).
+ */
+public class ConstructorReturnTypeRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!functionNode.isConstructor()) {
+ return;
+ }
+ IExpressionNode returnTypeNode = functionNode.getReturnTypeNode();
+ if (returnTypeNode == null) {
+ return;
+ }
+ problems.add(new ConstructorReturnTypeLinterProblem(functionNode));
+ }
+
+ public static class ConstructorReturnTypeLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Constructor '${functionName}' must not specify '${returnType}' return type";
+
+ public ConstructorReturnTypeLinterProblem(IFunctionNode node)
+ {
+ super(node.getNameExpressionNode());
+ functionName = node.getName();
+ returnType = node.getReturnType();
+ }
+
+ public String functionName;
+ public String returnType;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/DynamicClassRule.java b/linter/src/main/java/org/apache/royale/linter/rules/DynamicClassRule.java
new file mode 100644
index 0000000..6df5ee9
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/DynamicClassRule.java
@@ -0,0 +1,66 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.common.ASModifier;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IClassNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that symbols in package or class scopes have a namespace.
+ */
+public class DynamicClassRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.ClassID, (node, tokenQuery, problems) -> {
+ checkClassNode((IClassNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkClassNode(IClassNode classNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!classNode.hasModifier(ASModifier.DYNAMIC)) {
+ return;
+ }
+ problems.add(new DynamicClassLinterProblem(classNode));
+ }
+
+ public static class DynamicClassLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Class '${className}' must not be dynamic";
+
+ public DynamicClassLinterProblem(IClassNode node)
+ {
+ super(node);
+ className = node.getName();
+ }
+
+ public String className;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/EmptyCommentRule.java b/linter/src/main/java/org/apache/royale/linter/rules/EmptyCommentRule.java
new file mode 100644
index 0000000..177c45b
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/EmptyCommentRule.java
@@ -0,0 +1,109 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.common.ISourceLocation;
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.parsing.IMXMLToken;
+import org.apache.royale.compiler.parsing.IMXMLToken.MXMLTokenKind;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.MXMLTokenQuery;
+import org.apache.royale.linter.MXMLTokenVisitor;
+import org.apache.royale.linter.TokenQuery;
+import org.apache.royale.linter.TokenVisitor;
+
+/**
+ * Checks for line or block comments that are empty.
+ */
+public class EmptyCommentRule extends LinterRule {
+ @Override
+ public Map<Integer, TokenVisitor> getTokenVisitors() {
+ Map<Integer, TokenVisitor> result = new HashMap<>();
+ result.put(ASTokenTypes.HIDDEN_TOKEN_SINGLE_LINE_COMMENT, (token, tokenQuery, problems) -> {
+ checkSingleLineComment(token, tokenQuery, problems);
+ });
+ result.put(ASTokenTypes.HIDDEN_TOKEN_MULTI_LINE_COMMENT, (token, tokenQuery, problems) -> {
+ checkMultiLineComment(token, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ @Override
+ public Map<MXMLTokenKind, MXMLTokenVisitor> getMXMLTokenVisitors() {
+ Map<MXMLTokenKind, MXMLTokenVisitor> result = new HashMap<>();
+ result.put(MXMLTokenKind.COMMENT, (token, tokenQuery, problems) -> {
+ checkMXMLComment(token, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkSingleLineComment(IASToken comment, TokenQuery tokenQuery,
+ Collection<ICompilerProblem> problems) {
+ String commentText = comment.getText();
+ commentText = commentText.substring(2).trim();
+ if (commentText.length() > 0) {
+ return;
+ }
+ problems.add(new EmptyCommentLinterProblem(comment));
+ }
+
+ private void checkMultiLineComment(IASToken comment, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String commentText = comment.getText();
+ commentText = commentText.substring(2, commentText.length() - 2).trim();
+ if (commentText.length() > 0) {
+ return;
+ }
+ problems.add(new EmptyCommentLinterProblem(comment));
+ }
+
+ private void checkMXMLComment(IMXMLToken comment, MXMLTokenQuery tokenQuery,
+ Collection<ICompilerProblem> problems) {
+ String commentText = comment.getText();
+ boolean isASDoc = commentText.startsWith("<!---") && commentText.length() > 7;
+ if (isASDoc) {
+ // see ASDocRule
+ return;
+ }
+ commentText = commentText.substring(4, commentText.length() - 3).trim();
+ if (commentText.length() > 0) {
+ return;
+ }
+ problems.add(new EmptyCommentLinterProblem(comment));
+ }
+
+ public static class EmptyCommentLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Comment must not be empty";
+
+ public EmptyCommentLinterProblem(IASToken token) {
+ super(token);
+ }
+
+ public EmptyCommentLinterProblem(IMXMLToken token) {
+ super((ISourceLocation) token);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/EmptyFunctionBodyRule.java b/linter/src/main/java/org/apache/royale/linter/rules/EmptyFunctionBodyRule.java
new file mode 100644
index 0000000..bfa04a6
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/EmptyFunctionBodyRule.java
@@ -0,0 +1,92 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IBlockNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for function bodies that are empty.
+ */
+public class EmptyFunctionBodyRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (functionNode.isConstructor()) {
+ return;
+ }
+ IBlockNode blockNode = getBody(functionNode);
+ if (blockNode == null) {
+ return;
+ }
+ if (!isEmptyBlock(blockNode, tokenQuery)) {
+ return;
+ }
+ problems.add(new EmptyFunctionBodyLinterProblem(blockNode));
+ }
+
+ private IBlockNode getBody(IFunctionNode functionNode) {
+ if (functionNode.getChildCount() == 0) {
+ return null;
+ }
+ IASNode lastChild = functionNode.getChild(functionNode.getChildCount() - 1);
+ if (lastChild instanceof IBlockNode) {
+ return (IBlockNode) lastChild;
+ }
+ return null;
+ }
+
+ private boolean isEmptyBlock(IBlockNode blockNode, TokenQuery tokenQuery) {
+ if (blockNode.getChildCount() > 0) {
+ return false;
+ }
+ if (tokenQuery.getCommentsInside(blockNode).length > 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public static class EmptyFunctionBodyLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Function body must not be empty";
+
+ public EmptyFunctionBodyLinterProblem(IBlockNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/EmptyNestedBlockRule.java b/linter/src/main/java/org/apache/royale/linter/rules/EmptyNestedBlockRule.java
new file mode 100644
index 0000000..87319ce
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/EmptyNestedBlockRule.java
@@ -0,0 +1,88 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IBlockNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IPackageNode;
+import org.apache.royale.compiler.tree.as.ITypeNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for empty blocks, as long as they aren't the bodies of classes,
+ * interfaces, or packages.
+ */
+public class EmptyNestedBlockRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.BlockID, (node, tokenQuery, problems) -> {
+ checkBlockNode((IBlockNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkBlockNode(IBlockNode blockNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!isNested(blockNode)) {
+ return;
+ }
+ if (!isEmptyBlock(blockNode, tokenQuery)) {
+ return;
+ }
+ problems.add(new EmptyNestedBlockLinterProblem(blockNode));
+ }
+
+ private boolean isNested(IBlockNode blockNode) {
+ IASNode parentNode = blockNode.getParent();
+ return parentNode != null
+ && !(parentNode instanceof IPackageNode)
+ && !(parentNode instanceof ITypeNode)
+ && !(parentNode instanceof IFunctionNode);
+ }
+
+ private boolean isEmptyBlock(IBlockNode blockNode, TokenQuery tokenQuery) {
+ if (blockNode.getChildCount() > 0) {
+ return false;
+ }
+ if (tokenQuery.getCommentsInside(blockNode).length > 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public static class EmptyNestedBlockLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Block must not be empty";
+
+ public EmptyNestedBlockLinterProblem(IBlockNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/EmptyStatementRule.java b/linter/src/main/java/org/apache/royale/linter/rules/EmptyStatementRule.java
new file mode 100644
index 0000000..ccfb3f8
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/EmptyStatementRule.java
@@ -0,0 +1,70 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.TokenQuery;
+import org.apache.royale.linter.TokenVisitor;
+
+/**
+ * Checks for empty statements. An empty statement consists of only a semicolon
+ * (;) character.
+ */
+public class EmptyStatementRule extends LinterRule {
+ @Override
+ public Map<Integer, TokenVisitor> getTokenVisitors() {
+ Map<Integer, TokenVisitor> result = new HashMap<>();
+ result.put(ASTokenTypes.TOKEN_SEMICOLON, (token, tokenQuery, problems) -> {
+ checkSemicolon(token, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkSemicolon(IASToken semicolon, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IASToken prevToken = tokenQuery.getTokenBefore(semicolon);
+ if (prevToken == null) {
+ return;
+ }
+ if (prevToken.getType() != ASTokenTypes.TOKEN_SEMICOLON) {
+ return;
+ }
+ if (prevToken.isImplicit()) {
+ return;
+ }
+ problems.add(new EmptyStatementLinterProblem(semicolon));
+ }
+
+ public static class EmptyStatementLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Statement must not be empty";
+
+ public EmptyStatementLinterProblem(IASToken token)
+ {
+ super(token);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/FieldNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/FieldNameRule.java
new file mode 100644
index 0000000..974613e
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/FieldNameRule.java
@@ -0,0 +1,93 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IScopedNode;
+import org.apache.royale.compiler.tree.as.ITypeNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that field names match a specific pattern.
+ */
+public class FieldNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[_a-z][a-zA-Z0-9]*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (variableNode.isConst()) {
+ return;
+ }
+ IScopedNode containingScope = variableNode.getContainingScope();
+ if (containingScope == null) {
+ return;
+ }
+ IASNode possibleType = containingScope.getParent();
+ if (!(possibleType instanceof ITypeNode)) {
+ return;
+ }
+ String variableName = variableNode.getName();
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(variableName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new FieldNameLinterProblem(variableNode, thePattern));
+ }
+
+ public static class FieldNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Field name '${varName}' does not match the pattern '${pattern}'";
+
+ public FieldNameLinterProblem(IVariableNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ varName = node.getName();
+ }
+
+ public String pattern;
+ public String varName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/FunctionNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/FunctionNameRule.java
new file mode 100644
index 0000000..9cb10db
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/FunctionNameRule.java
@@ -0,0 +1,86 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that function names match a specific pattern.
+ */
+public class FunctionNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[a-z][a-zA-Z0-9]*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (functionNode.isConstructor()) {
+ // checked by ClassNameRule
+ return;
+ }
+ String functionName = functionNode.getName();
+ if (functionName == null || functionName.length() == 0) {
+ return;
+ }
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(functionName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new FunctionNameLinterProblem(functionNode, thePattern));
+ }
+
+ public static class FunctionNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Function name '${functionName}' does not match the pattern '${pattern}'";
+
+ public FunctionNameLinterProblem(IFunctionNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ functionName = node.getName();
+ }
+
+ public String pattern;
+ public String functionName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/IfBooleanLiteralRule.java b/linter/src/main/java/org/apache/royale/linter/rules/IfBooleanLiteralRule.java
new file mode 100644
index 0000000..b569399
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/IfBooleanLiteralRule.java
@@ -0,0 +1,73 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IIfNode;
+import org.apache.royale.compiler.tree.as.ILiteralNode;
+import org.apache.royale.compiler.tree.as.ILiteralNode.LiteralType;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that a boolean literal value is not used as an 'if' condition.
+ */
+public class IfBooleanLiteralRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.IfStatementID, (node, tokenQuery, problems) -> {
+ checkIfNode((IIfNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkIfNode(IIfNode ifNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IExpressionNode conditionalNode = ifNode.getConditionalExpressionNode();
+ if (!(conditionalNode instanceof ILiteralNode)) {
+ return;
+ }
+ ILiteralNode literalNode = (ILiteralNode) conditionalNode;
+ if (!LiteralType.BOOLEAN.equals(literalNode.getLiteralType())) {
+ return;
+ }
+ problems.add(new IfBooleanLiteralLinterProblem(literalNode));
+ }
+
+ public static class IfBooleanLiteralLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Condition is always '${value}'";
+
+ public IfBooleanLiteralLinterProblem(ILiteralNode node)
+ {
+ super(node);
+ value = node.getValue();
+ }
+
+ public String value;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/InterfaceNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/InterfaceNameRule.java
new file mode 100644
index 0000000..23a4d27
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/InterfaceNameRule.java
@@ -0,0 +1,77 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IInterfaceNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that interface names match a specific pattern.
+ */
+public class InterfaceNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^I[A-Z][a-zA-Z0-9]*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.InterfaceID, (node, tokenQuery, problems) -> {
+ checkInterfaceNode((IInterfaceNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkInterfaceNode(IInterfaceNode interfaceNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String interfaceName = interfaceNode.getName();
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(interfaceName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new InterfaceNameLinterProblem(interfaceNode, thePattern));
+ }
+
+ public static class InterfaceNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Interface name does not match the pattern '${pattern}'";
+
+ public InterfaceNameLinterProblem(IInterfaceNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ }
+
+ public String pattern;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/LineCommentPositionRule.java b/linter/src/main/java/org/apache/royale/linter/rules/LineCommentPositionRule.java
new file mode 100644
index 0000000..ae3fc4b
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/LineCommentPositionRule.java
@@ -0,0 +1,79 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.TokenQuery;
+import org.apache.royale.linter.TokenVisitor;
+import org.apache.royale.linter.config.LineCommentPosition;
+
+/**
+ * Checks if line comments appear beside code on the same line, or on a
+ * separate line.
+ */
+public class LineCommentPositionRule extends LinterRule {
+ @Override
+ public Map<Integer, TokenVisitor> getTokenVisitors() {
+ Map<Integer, TokenVisitor> result = new HashMap<>();
+ result.put(ASTokenTypes.HIDDEN_TOKEN_SINGLE_LINE_COMMENT, (token, tokenQuery, problems) -> {
+ checkSingleLineComment(token, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public LineCommentPosition position = LineCommentPosition.ABOVE;
+
+ private void checkSingleLineComment(IASToken comment, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IASToken prevToken = tokenQuery.getTokenBefore(comment);
+ if (prevToken == null) {
+ return;
+ }
+ if (LineCommentPosition.ABOVE.equals(position)) {
+ if (prevToken.getLine() == comment.getLine()) {
+ problems.add(new LineCommentPositionLinterProblem(comment, position));
+ }
+ }
+ else if (LineCommentPosition.BESIDE.equals(position)) {
+ if (prevToken.getLine() != comment.getLine()) {
+ problems.add(new LineCommentPositionLinterProblem(comment, position));
+ }
+ }
+ }
+
+ public static class LineCommentPositionLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Comment must be ${position} code";
+
+ public LineCommentPositionLinterProblem(IASToken token, LineCommentPosition position)
+ {
+ super(token);
+ this.position = position.getPosition();
+ }
+
+ public String position;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/LocalVarAndParameterNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/LocalVarAndParameterNameRule.java
new file mode 100644
index 0000000..6a1be14
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/LocalVarAndParameterNameRule.java
@@ -0,0 +1,125 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IPackageNode;
+import org.apache.royale.compiler.tree.as.IParameterNode;
+import org.apache.royale.compiler.tree.as.IScopedNode;
+import org.apache.royale.compiler.tree.as.ITypeNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that local variable and parameter names match a specific pattern.
+ */
+public class LocalVarAndParameterNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[a-z][a-zA-Z0-9]*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.ArgumentID, (node, tokenQuery, problems) -> {
+ checkParameterNode((IParameterNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (variableNode.isConst()) {
+ return;
+ }
+ IScopedNode containingScope = variableNode.getContainingScope();
+ if (containingScope == null) {
+ return;
+ }
+ IASNode possibleType = containingScope.getParent();
+ if (possibleType instanceof ITypeNode || possibleType instanceof IPackageNode) {
+ return;
+ }
+ String variableName = variableNode.getName();
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(variableName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new LocalVarNameLinterProblem(variableNode, thePattern));
+ }
+
+ private void checkParameterNode(IParameterNode paramNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String variableName = paramNode.getName();
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(variableName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new ParameterNameLinterProblem(paramNode, thePattern));
+ }
+
+ public static class LocalVarNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Variable name '${varName}' does not match the pattern '${pattern}'";
+
+ public LocalVarNameLinterProblem(IVariableNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ varName = node.getName();
+ }
+
+ public String pattern;
+ public String varName;
+ }
+
+ public static class ParameterNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Parameter name '${paramName}' does not match the pattern '${pattern}'";
+
+ public ParameterNameLinterProblem(IVariableNode node, Pattern pattern)
+ {
+ super(node.getNameExpressionNode());
+ this.pattern = pattern.toString();
+ paramName = node.getName();
+ }
+
+ public String pattern;
+ public String paramName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/LocalVarShadowsFieldRule.java b/linter/src/main/java/org/apache/royale/linter/rules/LocalVarShadowsFieldRule.java
new file mode 100644
index 0000000..4fce9ce
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/LocalVarShadowsFieldRule.java
@@ -0,0 +1,88 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IClassNode;
+import org.apache.royale.compiler.tree.as.IPackageNode;
+import org.apache.royale.compiler.tree.as.IScopedNode;
+import org.apache.royale.compiler.tree.as.ITypeNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+public class LocalVarShadowsFieldRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IScopedNode containingScope = variableNode.getContainingScope();
+ if (containingScope == null) {
+ return;
+ }
+ IASNode possibleType = containingScope.getParent();
+ if (possibleType instanceof ITypeNode || possibleType instanceof IPackageNode) {
+ return;
+ }
+ IClassNode containingClass = (IClassNode) variableNode.getAncestorOfType(IClassNode.class);
+ if (containingClass == null) {
+ return;
+ }
+ IScopedNode classScope = containingClass.getScopedNode();
+ for(int i = 0; i < classScope.getChildCount(); i++) {
+ IASNode child = classScope.getChild(i);
+ if (child instanceof IVariableNode) {
+ IVariableNode childVar = (IVariableNode) child;
+ if (variableNode.getName().equals(childVar.getName())) {
+ problems.add(new LocalVarShadowsFieldLinterProblem(variableNode, containingClass));
+ return;
+ }
+ }
+ }
+ }
+
+ public static class LocalVarShadowsFieldLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Local variable '${varName}' has the same name as field in class '${className}'";
+
+ public LocalVarShadowsFieldLinterProblem(IVariableNode variableNode, IClassNode classNode)
+ {
+ super(variableNode.getNameExpressionNode());
+ varName = variableNode.getName();
+ className = classNode.getName();
+ }
+
+ public String varName;
+ public String className;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MXMLEmptyAttributeRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MXMLEmptyAttributeRule.java
new file mode 100644
index 0000000..147ea2b
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MXMLEmptyAttributeRule.java
@@ -0,0 +1,65 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.royale.compiler.mxml.IMXMLTagAttributeData;
+import org.apache.royale.compiler.mxml.IMXMLTagData;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.MXMLTagVisitor;
+import org.apache.royale.linter.MXMLTokenQuery;
+
+/**
+ * Check that MXML attribute values are not empty.
+ */
+public class MXMLEmptyAttributeRule extends LinterRule {
+ @Override
+ public List<MXMLTagVisitor> getMXMLTagVisitors() {
+ List<MXMLTagVisitor> result = new ArrayList<>();
+ result.add((tag, tokenQuery, problems) -> {
+ checkTag(tag, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkTag(IMXMLTagData tag, MXMLTokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ for (IMXMLTagAttributeData attribute : tag.getAttributeDatas()) {
+ if (attribute.getRawValue().trim().length() == 0) {
+ problems.add(new MXMLEmptyAttributeLinterProblem(attribute));
+ }
+ }
+ }
+
+ public static class MXMLEmptyAttributeLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "MXML attribute '${attributeName}' value is empty";
+
+ public MXMLEmptyAttributeLinterProblem(IMXMLTagAttributeData attribute) {
+ super(attribute);
+ this.attributeName = attribute.getName();
+ }
+
+ public String attributeName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MXMLIDRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MXMLIDRule.java
new file mode 100644
index 0000000..4287044
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MXMLIDRule.java
@@ -0,0 +1,82 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.mxml.IMXMLTagAttributeData;
+import org.apache.royale.compiler.mxml.IMXMLTagData;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.MXMLTagVisitor;
+import org.apache.royale.linter.MXMLTokenQuery;
+
+/**
+ * Check that MXML id attribute values match a specific pattern.
+ */
+public class MXMLIDRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[a-z][a-zA-Z0-9]*$");
+
+ @Override
+ public List<MXMLTagVisitor> getMXMLTagVisitors() {
+ List<MXMLTagVisitor> result = new ArrayList<>();
+ result.add((tag, tokenQuery, problems) -> {
+ checkTag(tag, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkTag(IMXMLTagData tag, MXMLTokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IMXMLTagAttributeData idAttribute = tag.getTagAttributeData("id");
+ if (idAttribute == null) {
+ return;
+ }
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(idAttribute.getRawValue());
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new MXMLIDLinterProblem(idAttribute, thePattern));
+ }
+
+ public static class MXMLIDLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "MXML id '${idValue}' does not match the pattern '${pattern}'";
+
+ public MXMLIDLinterProblem(IMXMLTagAttributeData attribute, Pattern pattern)
+ {
+ super(attribute);
+ this.pattern = pattern.toString();
+ this.idValue = attribute.getRawValue();
+ }
+
+ public String pattern;
+ public String idValue;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MaxBlockDepthRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MaxBlockDepthRule.java
new file mode 100644
index 0000000..49aa594
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MaxBlockDepthRule.java
@@ -0,0 +1,113 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IBlockNode;
+import org.apache.royale.compiler.tree.as.IConditionalNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IStatementNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks the number of nested blocks in a function.
+ */
+public class MaxBlockDepthRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public int maximum = 4;
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ int maxBlockDepth = getMaxDepth(functionNode.getScopedNode());
+ if (maxBlockDepth <= maximum) {
+ return;
+ }
+ problems.add(new MaxBlockDepthLinterProblem(functionNode, maxBlockDepth, maximum));
+ }
+
+ private int getMaxDepth(IASNode node) {
+ int maxDepth = 0;
+ for(int i = 0; i < node.getChildCount(); i++) {
+ IASNode child = node.getChild(i);
+ if (child instanceof IFunctionNode) {
+ // nested functions don't count
+ continue;
+ }
+ if (child instanceof IStatementNode) {
+ IStatementNode statementNode = (IStatementNode) child;
+ IASNode possibleBlock = null;
+ if (statementNode.getChildCount() > 0) {
+ possibleBlock = statementNode.getChild(statementNode.getChildCount() - 1);
+ if (possibleBlock instanceof IConditionalNode) {
+ IConditionalNode conditionalNode = (IConditionalNode) possibleBlock;
+ if (conditionalNode.getChildCount() > 0) {
+ possibleBlock = conditionalNode.getChild(conditionalNode.getChildCount() - 1);
+ }
+ }
+ }
+ if (possibleBlock instanceof IBlockNode) {
+ int childMaxDepth = getMaxDepth(possibleBlock) + 1;
+ if (maxDepth < childMaxDepth) {
+ maxDepth = childMaxDepth;
+ }
+ }
+ }
+ else if (child instanceof IBlockNode) {
+ int childMaxDepth = getMaxDepth(child) + 1;
+ if (maxDepth < childMaxDepth) {
+ maxDepth = childMaxDepth;
+ }
+ }
+ }
+ return maxDepth;
+ }
+
+ public static class MaxBlockDepthLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Function '${functionName}' has blocks nested ${depth} levels deep, but expected no deeper than ${maxDepth} levels";
+
+ public MaxBlockDepthLinterProblem(IFunctionNode node, int depth, int maxDepth)
+ {
+ super(node.getParametersContainerNode());
+ this.depth = depth;
+ this.maxDepth = maxDepth;
+ functionName = node.getName();
+ }
+
+ public String functionName;
+ public int depth;
+ public int maxDepth;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MaxParametersRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MaxParametersRule.java
new file mode 100644
index 0000000..b2005b3
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MaxParametersRule.java
@@ -0,0 +1,71 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks the number of function parameters.
+ */
+public class MaxParametersRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public int maximum = 6;
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (functionNode.getParameterNodes().length <= maximum) {
+ return;
+ }
+ problems.add(new MaxParametersLinterProblem(functionNode, maximum));
+ }
+
+ public static class MaxParametersLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Function '${functionName}' has ${params} parameters, but expected no more than ${maxParams} parameters";
+
+ public MaxParametersLinterProblem(IFunctionNode node, int maxParams)
+ {
+ super(node.getParametersContainerNode());
+ this.maxParams = maxParams;
+ params = node.getParameterNodes().length;
+ functionName = node.getName();
+ }
+
+ public String functionName;
+ public int params;
+ public int maxParams;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MissingASDocRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MissingASDocRule.java
new file mode 100644
index 0000000..5459afd
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MissingASDocRule.java
@@ -0,0 +1,140 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.common.ISourceLocation;
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.parsing.IMXMLToken;
+import org.apache.royale.compiler.parsing.IMXMLToken.MXMLTokenKind;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IDocumentableDefinitionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.MXMLTokenQuery;
+import org.apache.royale.linter.MXMLTokenVisitor;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for missing or empty ASDoc comments.
+ */
+public class MissingASDocRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.ClassID, (node, tokenQuery, problems) -> {
+ checkDocumentableDefinitionNode((IDocumentableDefinitionNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.InterfaceID, (node, tokenQuery, problems) -> {
+ checkDocumentableDefinitionNode((IDocumentableDefinitionNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkDocumentableDefinitionNode((IDocumentableDefinitionNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkDocumentableDefinitionNode((IDocumentableDefinitionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ @Override
+ public Map<MXMLTokenKind, MXMLTokenVisitor> getMXMLTokenVisitors() {
+ Map<MXMLTokenKind, MXMLTokenVisitor> result = new HashMap<>();
+ result.put(MXMLTokenKind.COMMENT, (token, tokenQuery, problems) -> {
+ checkMXMLComment(token, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkDocumentableDefinitionNode(IDocumentableDefinitionNode definitionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!definitionNode.hasNamespace("public")) {
+ return;
+ }
+ IASToken token = tokenQuery.getTokenBefore(definitionNode);
+ if (token.getType() == ASTokenTypes.TOKEN_ASDOC_COMMENT) {
+ String docComment = token.getText();
+ if (!isDocCommentEmpty(docComment)) {
+ return;
+ }
+ problems.add(new EmptyASDocLinterProblem(token));
+ return;
+ }
+ problems.add(new MissingASDocLinterProblem(definitionNode));
+ }
+
+ private boolean isDocCommentEmpty(String docComment) {
+ docComment = docComment.substring(3, docComment.length() - 2).trim();
+ String[] lines = docComment.split("\\r?\\n");
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[0];
+ line = line.trim();
+ if (line.startsWith("*")) {
+ line = line.substring(1).trim();
+ }
+ if (line.length() > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private void checkMXMLComment(IMXMLToken comment, MXMLTokenQuery tokenQuery,
+ Collection<ICompilerProblem> problems) {
+ String commentText = comment.getText();
+ boolean isASDoc = commentText.startsWith("<!---") && commentText.length() > 7;
+ if (!isASDoc) {
+ return;
+ }
+ commentText = commentText.substring(5, commentText.length() - 3).trim();
+ if (commentText.length() > 0) {
+ return;
+ }
+ problems.add(new EmptyASDocLinterProblem(comment));
+ }
+
+ public static class MissingASDocLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Public APIs must have ASDoc comments";
+
+ public MissingASDocLinterProblem(IDocumentableDefinitionNode node)
+ {
+ super(node);
+ }
+ }
+
+ public static class EmptyASDocLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "ASDoc comments must not be empty";
+
+ public EmptyASDocLinterProblem(IASToken token)
+ {
+ super(token);
+ }
+
+ public EmptyASDocLinterProblem(IMXMLToken token)
+ {
+ super((ISourceLocation) token);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MissingNamespaceRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MissingNamespaceRule.java
new file mode 100644
index 0000000..1d02626
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MissingNamespaceRule.java
@@ -0,0 +1,219 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.tree.as.BaseDefinitionNode;
+import org.apache.royale.compiler.internal.tree.as.IdentifierNode;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IClassNode;
+import org.apache.royale.compiler.tree.as.IDefinitionNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IFunctionObjectNode;
+import org.apache.royale.compiler.tree.as.IInterfaceNode;
+import org.apache.royale.compiler.tree.as.INamespaceDecorationNode;
+import org.apache.royale.compiler.tree.as.IPackageNode;
+import org.apache.royale.compiler.tree.as.IScopedNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that symbols in package or class scopes have a namespace.
+ */
+public class MissingNamespaceRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.ClassID, (node, tokenQuery, problems) -> {
+ checkClassNode((IClassNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.InterfaceID, (node, tokenQuery, problems) -> {
+ checkInterfaceNode((IInterfaceNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkClassNode(IClassNode classNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IScopedNode scopedNode = classNode.getContainingScope();
+ IASNode possiblePackage = scopedNode.getParent();
+ if (!(possiblePackage instanceof IPackageNode)) {
+ return;
+ }
+ if (hasNamespace(classNode)) {
+ return;
+ }
+ problems.add(new MissingNamespaceOnClassLinterProblem(classNode));
+ }
+
+ private void checkInterfaceNode(IInterfaceNode interfaceNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IScopedNode scopedNode = interfaceNode.getContainingScope();
+ IASNode possiblePackage = scopedNode.getParent();
+ if (!(possiblePackage instanceof IPackageNode)) {
+ return;
+ }
+ if (hasNamespace(interfaceNode)) {
+ return;
+ }
+ problems.add(new MissingNamespaceOnInterfaceLinterProblem(interfaceNode));
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IScopedNode scopedNode = functionNode.getContainingScope();
+ IASNode possiblePackageOrClass = scopedNode.getParent();
+ if (possiblePackageOrClass instanceof IPackageNode) {
+ if (hasNamespace(functionNode) || functionNode.getParent() instanceof IFunctionObjectNode) {
+ return;
+ }
+ problems.add(new MissingNamespaceOnPackageFunctionLinterProblem(functionNode));
+ return;
+ }
+ if (possiblePackageOrClass instanceof IClassNode) {
+ if (hasNamespace(functionNode) || functionNode.getParent() instanceof IFunctionObjectNode) {
+ return;
+ }
+ problems.add(new MissingNamespaceOnMethodLinterProblem(functionNode));
+ return;
+ }
+ }
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IScopedNode scopedNode = variableNode.getContainingScope();
+ IASNode possiblePackageOrClass = scopedNode.getParent();
+ if (possiblePackageOrClass instanceof IPackageNode) {
+ if (hasNamespace(variableNode)) {
+ return;
+ }
+ problems.add(new MissingNamespaceOnPackageVariableLinterProblem(variableNode));
+ return;
+ }
+ if (possiblePackageOrClass instanceof IClassNode) {
+ if (hasNamespace(variableNode)) {
+ return;
+ }
+ problems.add(new MissingNamespaceOnFieldLinterProblem(variableNode));
+ return;
+ }
+ }
+
+ private boolean hasNamespace(IDefinitionNode node) {
+ if (node instanceof BaseDefinitionNode) {
+ BaseDefinitionNode baseDefNode = (BaseDefinitionNode) node;
+ INamespaceDecorationNode nsNode = baseDefNode.getNamespaceNode();
+ if (nsNode == null) {
+ return false;
+ }
+ if (nsNode instanceof IdentifierNode) {
+ IdentifierNode identifierNode = (IdentifierNode) nsNode;
+ if (identifierNode.isImplicit()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ String ns = node.getNamespace();
+ return ns != null;
+ }
+
+ public static class MissingNamespaceOnClassLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing namespace on class '${className}'";
+
+ public MissingNamespaceOnClassLinterProblem(IClassNode node)
+ {
+ super(node);
+ className = node.getName();
+ }
+
+ public String className;
+ }
+
+ public static class MissingNamespaceOnInterfaceLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing namespace on interface '${interfaceName}'";
+
+ public MissingNamespaceOnInterfaceLinterProblem(IInterfaceNode node)
+ {
+ super(node);
+ interfaceName = node.getName();
+ }
+
+ public String interfaceName;
+ }
+
+ public static class MissingNamespaceOnPackageFunctionLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing namespace on package function '${functionName}'";
+
+ public MissingNamespaceOnPackageFunctionLinterProblem(IFunctionNode node)
+ {
+ super(node);
+ functionName = node.getQualifiedName();
+ }
+
+ public String functionName;
+ }
+
+ public static class MissingNamespaceOnMethodLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing namespace on method '${functionName}'";
+
+ public MissingNamespaceOnMethodLinterProblem(IFunctionNode node)
+ {
+ super(node);
+ functionName = node.getName();
+ }
+
+ public String functionName;
+ }
+
+ public static class MissingNamespaceOnPackageVariableLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing namespace on package variable '${variableName}'";
+
+ public MissingNamespaceOnPackageVariableLinterProblem(IVariableNode node)
+ {
+ super(node);
+ variableName = node.getQualifiedName();
+ }
+
+ public String variableName;
+ }
+
+ public static class MissingNamespaceOnFieldLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing namespace on field '${variableName}'";
+
+ public MissingNamespaceOnFieldLinterProblem(IVariableNode node)
+ {
+ super(node);
+ variableName = node.getName();
+ }
+
+ public String variableName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MissingSemicolonRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MissingSemicolonRule.java
new file mode 100644
index 0000000..50a5371
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MissingSemicolonRule.java
@@ -0,0 +1,62 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.TokenQuery;
+import org.apache.royale.linter.TokenVisitor;
+
+/**
+ * Checks for statements with missing semicolons.
+ */
+public class MissingSemicolonRule extends LinterRule {
+ @Override
+ public Map<Integer, TokenVisitor> getTokenVisitors() {
+ Map<Integer, TokenVisitor> result = new HashMap<>();
+ result.put(ASTokenTypes.TOKEN_SEMICOLON, (token, tokenQuery, problems) -> {
+ checkSemicolon(token, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkSemicolon(IASToken semicolon, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!semicolon.isImplicit()) {
+ return;
+ }
+ problems.add(new MissingSemicolonLinterProblem(semicolon));
+ }
+
+ public static class MissingSemicolonLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Semicolon is required";
+
+ public MissingSemicolonLinterProblem(IASToken token)
+ {
+ super(token);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/MissingTypeRule.java b/linter/src/main/java/org/apache/royale/linter/rules/MissingTypeRule.java
new file mode 100644
index 0000000..1b19034
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/MissingTypeRule.java
@@ -0,0 +1,129 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.tree.as.IdentifierNode;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IParameterNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks that a type has been declared for all variables, function parameters,
+ * and function returns.
+ */
+public class MissingTypeRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IExpressionNode typeNode = variableNode.getVariableTypeNode();
+ if (isValidTypeNode(typeNode)) {
+ return;
+ }
+ problems.add(new MissingVariableTypeLinterProblem(variableNode));
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ for (IParameterNode paramNode : functionNode.getParameterNodes()) {
+ if (paramNode.isRest()) {
+ //it's okay for a rest parameter not to have a type
+ continue;
+ }
+ IExpressionNode typeNode = paramNode.getVariableTypeNode();
+ if (!isValidTypeNode(typeNode)) {
+ problems.add(new MissingFunctionParameterTypeLinterProblem(paramNode));
+ }
+ }
+ if (functionNode.isConstructor()) {
+ return;
+ }
+ IExpressionNode typeNode = functionNode.getReturnTypeNode();
+ if (!isValidTypeNode(typeNode)) {
+ problems.add(new MissingFunctionReturnTypeLinterProblem(functionNode));
+ }
+ }
+
+ private boolean isValidTypeNode(IExpressionNode typeNode) {
+ if (typeNode == null) {
+ return false;
+ }
+ if (typeNode instanceof IdentifierNode) {
+ IdentifierNode identifierNode = (IdentifierNode) typeNode;
+ // isImplicit() is not on the interface, for some reason
+ if (identifierNode.isImplicit()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static class MissingVariableTypeLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing type for variable '${varName}'";
+
+ public MissingVariableTypeLinterProblem(IVariableNode node)
+ {
+ super(node);
+ varName = node.getName();
+ }
+
+ public String varName;
+ }
+
+ public static class MissingFunctionParameterTypeLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing type for function parameter '${paramName}'";
+
+ public MissingFunctionParameterTypeLinterProblem(IParameterNode node)
+ {
+ super(node);
+ paramName = node.getName();
+ }
+
+ public String paramName;
+ }
+
+ public static class MissingFunctionReturnTypeLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing function return type";
+
+ public MissingFunctionReturnTypeLinterProblem(IFunctionNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/NumericLeadingZeroesRule.java b/linter/src/main/java/org/apache/royale/linter/rules/NumericLeadingZeroesRule.java
new file mode 100644
index 0000000..ac07172
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/NumericLeadingZeroesRule.java
@@ -0,0 +1,79 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.common.ISourceLocation;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.INumericLiteralNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for use of numeric literals with leading zeroes, except when it starts
+ * with '0x' (hexadecimal notation). '0123' is not allowed. '0x123' is allowed.
+ *
+ * Designed to prevent confusion with ECMAScript's deprecated octal notation.
+ */
+public class NumericLeadingZeroesRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.LiteralIntegerID, (node, tokenQuery, problems) -> {
+ checkNumericLiteralNode((INumericLiteralNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.LiteralNumberID, (node, tokenQuery, problems) -> {
+ checkNumericLiteralNode((INumericLiteralNode) node, tokenQuery, problems);
+ });
+ result.put(ASTNodeID.LiteralUintID, (node, tokenQuery, problems) -> {
+ checkNumericLiteralNode((INumericLiteralNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkNumericLiteralNode(INumericLiteralNode numberNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String stringValue = numberNode.getNumericValue().toString();
+ if (!stringValue.startsWith("0")) {
+ return;
+ }
+ if (stringValue.startsWith("0x")) {
+ return;
+ }
+ problems.add(new NumericLeadingZeroesLinterProblem(numberNode));
+ }
+
+ public static class NumericLeadingZeroesLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must remove leading zeros from numeric literal '${value}'";
+
+ public NumericLeadingZeroesLinterProblem(INumericLiteralNode node)
+ {
+ super((ISourceLocation) node);
+ value = node.getNumericValue().toString();
+ }
+
+ public String value;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/OverrideContainsOnlySuperCallRule.java b/linter/src/main/java/org/apache/royale/linter/rules/OverrideContainsOnlySuperCallRule.java
new file mode 100644
index 0000000..2138aed
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/OverrideContainsOnlySuperCallRule.java
@@ -0,0 +1,103 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.common.ASModifier;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IFunctionCallNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.compiler.tree.as.IIdentifierNode;
+import org.apache.royale.compiler.tree.as.ILanguageIdentifierNode;
+import org.apache.royale.compiler.tree.as.ILanguageIdentifierNode.LanguageIdentifierKind;
+import org.apache.royale.compiler.tree.as.IMemberAccessExpressionNode;
+import org.apache.royale.compiler.tree.as.IScopedNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that an overridden function contains more than a call to super.
+ */
+public class OverrideContainsOnlySuperCallRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!functionNode.hasModifier(ASModifier.OVERRIDE)) {
+ return;
+ }
+ IScopedNode scopedNode = functionNode.getScopedNode();
+
+ if (scopedNode.getChildCount() == 0 || scopedNode.getChildCount() > 1) {
+ return;
+ }
+ IASNode child = scopedNode.getChild(0);
+ if (!(child instanceof IFunctionCallNode)) {
+ return;
+ }
+ IFunctionCallNode functionCallNode = (IFunctionCallNode) child;
+ IExpressionNode nameNode = functionCallNode.getNameNode();
+ if (!(nameNode instanceof IMemberAccessExpressionNode)) {
+ return;
+ }
+ IMemberAccessExpressionNode memberAccess = (IMemberAccessExpressionNode) nameNode;
+ if (!(memberAccess.getLeftOperandNode() instanceof ILanguageIdentifierNode)) {
+ return;
+ }
+ ILanguageIdentifierNode superNode = (ILanguageIdentifierNode) memberAccess.getLeftOperandNode();
+ if (!LanguageIdentifierKind.SUPER.equals(superNode.getKind())) {
+ return;
+ }
+ if (!(memberAccess.getRightOperandNode() instanceof IIdentifierNode)) {
+ return;
+ }
+ IIdentifierNode rightNode = (IIdentifierNode) memberAccess.getRightOperandNode();
+ if (!functionNode.getName().equals(rightNode.getName())) {
+ return;
+ }
+ problems.add(new OverrideContainsOnlySuperCallLinterProblem(functionNode));
+ }
+
+ public static class OverrideContainsOnlySuperCallLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Method override '${functionName}' must contain more than call to 'super.${functionName}'";
+
+ public OverrideContainsOnlySuperCallLinterProblem(IFunctionNode node)
+ {
+ super(node.getNameExpressionNode());
+ functionName = node.getName();
+ }
+
+ public String functionName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/PackageNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/PackageNameRule.java
new file mode 100644
index 0000000..de50e9a
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/PackageNameRule.java
@@ -0,0 +1,82 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IPackageNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that package names match a specific pattern.
+ */
+public class PackageNameRule extends LinterRule {
+ public static final Pattern DEFAULT_NAME_PATTERN = Pattern.compile("^[a-z][a-zA-Z0-9]*(\\.[a-z][a-zA-Z0-9]*)*$");
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.PackageID, (node, tokenQuery, problems) -> {
+ checkPackageNode((IPackageNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ public Pattern pattern;
+
+ private void checkPackageNode(IPackageNode packageNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String packageName = packageNode.getName();
+ if (packageName.length() == 0) {
+ return;
+ }
+ Pattern thePattern = pattern;
+ if (thePattern == null) {
+ thePattern = DEFAULT_NAME_PATTERN;
+ }
+ Matcher matcher = thePattern.matcher(packageName);
+ if (matcher.matches()) {
+ return;
+ }
+ problems.add(new PackageNameLinterProblem(packageNode, thePattern));
+ }
+
+ public static class PackageNameLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Package name '${packageName}' does not match the pattern '${pattern}'";
+
+ public PackageNameLinterProblem(IPackageNode node, Pattern pattern)
+ {
+ super(node);
+ this.pattern = pattern.toString();
+ packageName = node.getName();
+ }
+
+ public String pattern;
+ public String packageName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/StaticConstantsRule.java b/linter/src/main/java/org/apache/royale/linter/rules/StaticConstantsRule.java
new file mode 100644
index 0000000..59ddba9
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/StaticConstantsRule.java
@@ -0,0 +1,74 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.common.ASModifier;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IClassNode;
+import org.apache.royale.compiler.tree.as.IScopedNode;
+import org.apache.royale.compiler.tree.as.IVariableNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for constants that are declared on a class, but are not static.
+ */
+public class StaticConstantsRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.VariableID, (node, tokenQuery, problems) -> {
+ checkVariableNode((IVariableNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkVariableNode(IVariableNode variableNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!variableNode.isConst() || variableNode.hasModifier(ASModifier.STATIC)) {
+ return;
+ }
+ IScopedNode containingScope = variableNode.getContainingScope();
+ if (containingScope == null) {
+ return;
+ }
+ IASNode possibleClass = containingScope.getParent();
+ if (!(possibleClass instanceof IClassNode)) {
+ return;
+ }
+ problems.add(new StaticConstantsLinterProblem(variableNode));
+ }
+
+ public static class StaticConstantsLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Constant must be static";
+
+ public StaticConstantsLinterProblem(IVariableNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/StrictEqualityRule.java b/linter/src/main/java/org/apache/royale/linter/rules/StrictEqualityRule.java
new file mode 100644
index 0000000..457617c
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/StrictEqualityRule.java
@@ -0,0 +1,67 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.tree.as.IOperatorNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.TokenVisitor;
+
+/**
+ * Checks for uses of the '==' and '!='' operators instead of the stricter '==='
+ * and '!==' operators.
+ */
+public class StrictEqualityRule extends LinterRule {
+ @Override
+ public Map<Integer, TokenVisitor> getTokenVisitors() {
+ Map<Integer, TokenVisitor> result = new HashMap<>();
+ result.put(ASTokenTypes.TOKEN_OPERATOR_EQUAL, (token, tokenQuery, problems) -> {
+ problems.add(new StrictEqualityLinterProblem(token));
+ });
+ result.put(ASTokenTypes.TOKEN_OPERATOR_NOT_EQUAL, (token, tokenQuery, problems) -> {
+ problems.add(new StrictEqualityLinterProblem(token));
+ });
+ return result;
+ }
+
+ public static class StrictEqualityLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must use ${requiredTokenText} instead of ${tokenText}";
+
+ public StrictEqualityLinterProblem(IASToken token)
+ {
+ super(token);
+ tokenText = token.getText();
+ if (IOperatorNode.OperatorType.EQUAL.getOperatorText().equals(tokenText)) {
+ requiredTokenText = IOperatorNode.OperatorType.STRICT_EQUAL.getOperatorText();
+ }
+ else if (IOperatorNode.OperatorType.NOT_EQUAL.getOperatorText().equals(tokenText)) {
+ requiredTokenText = IOperatorNode.OperatorType.STRICT_NOT_EQUAL.getOperatorText();
+ }
+ }
+
+ public String tokenText;
+ public String requiredTokenText;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/StringEventNameRule.java b/linter/src/main/java/org/apache/royale/linter/rules/StringEventNameRule.java
new file mode 100644
index 0000000..8f0540c
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/StringEventNameRule.java
@@ -0,0 +1,107 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IExpressionNode;
+import org.apache.royale.compiler.tree.as.IFunctionCallNode;
+import org.apache.royale.compiler.tree.as.IIdentifierNode;
+import org.apache.royale.compiler.tree.as.ILiteralNode;
+import org.apache.royale.compiler.tree.as.ILiteralNode.LiteralType;
+import org.apache.royale.compiler.tree.as.IMemberAccessExpressionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check that calls to event dispatcher methods don't use string values for
+ * event names.
+ */
+public class StringEventNameRule extends LinterRule {
+ private static final String[] EVENT_DISPATCHER_FUNCTION_NAMES = {
+ "addEventListener",
+ "removeEventListener",
+ "hasEventListener"
+ };
+
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionCallID, (node, tokenQuery, problems) -> {
+ checkFunctionCallNode((IFunctionCallNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionCallNode(IFunctionCallNode functionCallNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ String functionName = null;
+ IExpressionNode nameNode = functionCallNode.getNameNode();
+ if (nameNode instanceof IIdentifierNode) {
+ IIdentifierNode identifierNode = (IIdentifierNode) nameNode;
+ functionName = identifierNode.getName();
+ }
+ else if(nameNode instanceof IMemberAccessExpressionNode) {
+ IMemberAccessExpressionNode memberAccessNode = (IMemberAccessExpressionNode) nameNode;
+ IExpressionNode rightNode = memberAccessNode.getRightOperandNode();
+ if (rightNode instanceof IIdentifierNode) {
+ IIdentifierNode identifierNode = (IIdentifierNode) rightNode;
+ functionName = identifierNode.getName();
+ }
+ }
+ if (functionName == null) {
+ return;
+ }
+ if (!Arrays.asList(EVENT_DISPATCHER_FUNCTION_NAMES).contains(functionName)) {
+ return;
+ }
+ IExpressionNode[] argumentNodes = functionCallNode.getArgumentNodes();
+ if (argumentNodes.length == 0) {
+ return;
+ }
+ IExpressionNode firstArgument = argumentNodes[0];
+ if (!(firstArgument instanceof ILiteralNode)) {
+ return;
+ }
+ ILiteralNode literalNode = (ILiteralNode) firstArgument;
+ if (!LiteralType.STRING.equals(literalNode.getLiteralType())) {
+ return;
+ }
+ problems.add(new StringEventNameProblem(functionCallNode, functionName));
+ }
+
+ public static class StringEventNameProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Calls to '${functionName}' must use constant value instead of string literal for event name";
+
+ public StringEventNameProblem(IFunctionCallNode node, String functionName)
+ {
+ super(node.getArgumentNodes()[0]);
+ this.functionName = functionName;
+ }
+
+ public String functionName;
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/SwitchWithoutDefaultRule.java b/linter/src/main/java/org/apache/royale/linter/rules/SwitchWithoutDefaultRule.java
new file mode 100644
index 0000000..3a9a7b6
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/SwitchWithoutDefaultRule.java
@@ -0,0 +1,64 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.ISwitchNode;
+import org.apache.royale.compiler.tree.as.ITerminalNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check for 'switch' statements that are missing a 'default' clause.
+ */
+public class SwitchWithoutDefaultRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.SwitchID, (node, tokenQuery, problems) -> {
+ checkSwitchNode((ISwitchNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkSwitchNode(ISwitchNode switchNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ ITerminalNode defaultNode = switchNode.getDefaultNode();
+ if (defaultNode != null) {
+ return;
+ }
+ problems.add(new SwitchWithoutDefaultLinterProblem(switchNode));
+ }
+
+ public static class SwitchWithoutDefaultLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Missing 'default' clause in 'switch' statement";
+
+ public SwitchWithoutDefaultLinterProblem(ISwitchNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/ThisInClosureRule.java b/linter/src/main/java/org/apache/royale/linter/rules/ThisInClosureRule.java
new file mode 100644
index 0000000..b994da6
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/ThisInClosureRule.java
@@ -0,0 +1,86 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IBlockNode;
+import org.apache.royale.compiler.tree.as.IFunctionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Checks for use of the 'this' keyword in closures.
+ */
+public class ThisInClosureRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionID, (node, tokenQuery, problems) -> {
+ checkFunctionNode((IFunctionNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionNode(IFunctionNode functionNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ IFunctionNode ancestorFunction = (IFunctionNode) functionNode.getAncestorOfType(IFunctionNode.class);
+ if (ancestorFunction == null) {
+ return;
+ }
+ IBlockNode blockNode = getBody(functionNode);
+ if (blockNode == null) {
+ return;
+ }
+ for(IASToken token : tokenQuery.getTokens(blockNode)) {
+ if (token.getType() == ASTokenTypes.TOKEN_KEYWORD_THIS) {
+ problems.add(new ThisInClosureLinterProblem(token));
+ }
+ }
+ }
+
+ private IBlockNode getBody(IFunctionNode functionNode) {
+ if (functionNode.getChildCount() == 0) {
+ return null;
+ }
+ IASNode lastChild = functionNode.getChild(functionNode.getChildCount() - 1);
+ if (lastChild instanceof IBlockNode) {
+ return (IBlockNode) lastChild;
+ }
+ return null;
+ }
+
+ public static class ThisInClosureLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Closure must not contain 'this' keyword";
+
+ public ThisInClosureLinterProblem(IASToken token)
+ {
+ super(token);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/TraceRule.java b/linter/src/main/java/org/apache/royale/linter/rules/TraceRule.java
new file mode 100644
index 0000000..bd5e6b6
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/TraceRule.java
@@ -0,0 +1,68 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IASNode;
+import org.apache.royale.compiler.tree.as.IFunctionCallNode;
+import org.apache.royale.compiler.tree.as.IMemberAccessExpressionNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check for calls to the 'trace()' function.
+ */
+public class TraceRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.FunctionCallID, (node, tokenQuery, problems) -> {
+ checkFunctionCallNode((IFunctionCallNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkFunctionCallNode(IFunctionCallNode functionCallNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!"trace".equals(functionCallNode.getFunctionName())) {
+ return;
+ }
+ IASNode parentNode = functionCallNode.getParent();
+ if (parentNode instanceof IMemberAccessExpressionNode) {
+ return;
+ }
+ problems.add(new TraceLinterProblem(functionCallNode));
+ }
+
+ public static class TraceLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must not call trace() function";
+
+ public TraceLinterProblem(IFunctionCallNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/WildcardImportRule.java b/linter/src/main/java/org/apache/royale/linter/rules/WildcardImportRule.java
new file mode 100644
index 0000000..8f7bf9f
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/WildcardImportRule.java
@@ -0,0 +1,62 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.compiler.problems.ICompilerProblem;
+import org.apache.royale.compiler.tree.ASTNodeID;
+import org.apache.royale.compiler.tree.as.IImportNode;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.NodeVisitor;
+import org.apache.royale.linter.TokenQuery;
+
+/**
+ * Check for import statements that import the entire package.
+ */
+public class WildcardImportRule extends LinterRule {
+ @Override
+ public Map<ASTNodeID, NodeVisitor> getNodeVisitors() {
+ Map<ASTNodeID, NodeVisitor> result = new HashMap<>();
+ result.put(ASTNodeID.ImportID, (node, tokenQuery, problems) -> {
+ checkImportNode((IImportNode) node, tokenQuery, problems);
+ });
+ return result;
+ }
+
+ private void checkImportNode(IImportNode importNode, TokenQuery tokenQuery, Collection<ICompilerProblem> problems) {
+ if (!importNode.isWildcardImport()) {
+ return;
+ }
+ problems.add(new WildcardImportLinterProblem(importNode));
+ }
+
+ public static class WildcardImportLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must not use wildcard import";
+
+ public WildcardImportLinterProblem(IImportNode node)
+ {
+ super(node);
+ }
+ }
+}
diff --git a/linter/src/main/java/org/apache/royale/linter/rules/WithRule.java b/linter/src/main/java/org/apache/royale/linter/rules/WithRule.java
new file mode 100644
index 0000000..840a98c
--- /dev/null
+++ b/linter/src/main/java/org/apache/royale/linter/rules/WithRule.java
@@ -0,0 +1,52 @@
+////////////////////////////////////////////////////////////////////////////////
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 org.apache.royale.linter.rules;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
+import org.apache.royale.compiler.parsing.IASToken;
+import org.apache.royale.compiler.problems.CompilerProblem;
+import org.apache.royale.linter.LinterRule;
+import org.apache.royale.linter.TokenVisitor;
+
+/**
+ * Checks for uses of 'with(x)'.
+ */
+public class WithRule extends LinterRule {
+ @Override
+ public Map<Integer, TokenVisitor> getTokenVisitors() {
+ Map<Integer, TokenVisitor> result = new HashMap<>();
+ result.put(ASTokenTypes.TOKEN_KEYWORD_WITH, (token, tokenQuery, problems) -> {
+ problems.add(new WithLinterProblem(token));
+ });
+ return result;
+ }
+
+ public static class WithLinterProblem extends CompilerProblem {
+ public static final String DESCRIPTION = "Must not use 'with' statement";
+
+ public WithLinterProblem(IASToken token)
+ {
+ super(token);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index f25b068..68f6a38 100644
--- a/pom.xml
+++ b/pom.xml
@@ -107,6 +107,7 @@
<module>swfutils</module>
<module>debugger</module>
<module>formatter</module>
+ <module>linter</module>
<module>flex-compiler-oem</module>
<module>royale-ant-tasks</module>
<module>royaleunit-ant-tasks</module>