View Javadoc
1   /*
2    * Copyright 2014-2015 Mark Prins, GeoDienstenCentrum
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package nl.geodienstencentrum.maven.plugin.sass.report;
17  
18  import com.google.common.primitives.Ints;
19  import java.io.File;
20  import java.util.ArrayList;
21  import java.util.EnumSet;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Set;
26  import javax.script.ScriptContext;
27  import javax.script.ScriptEngine;
28  import javax.script.ScriptEngineManager;
29  import javax.script.ScriptException;
30  import nl.geodienstencentrum.maven.plugin.sass.AbstractSassMojo;
31  import nl.geodienstencentrum.maven.plugin.sass.Resource;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.plugin.logging.Log;
35  import org.apache.maven.plugins.annotations.LifecyclePhase;
36  import org.apache.maven.plugins.annotations.Mojo;
37  import org.apache.maven.plugins.annotations.Parameter;
38  
39  /**
40   * SCSSLintMojo executes scss-lint goal.
41   *
42   * @author mprins
43   * @since 2.3
44   */
45  @Mojo(name = "scss-lint",
46  		defaultPhase = LifecyclePhase.COMPILE,
47  		executionStrategy = "once-per-session",
48  		threadSafe = true
49  	)
50  public class SCSSLintMojo extends AbstractSassMojo {
51  
52  	/**
53  	 * Output file for the plugin.
54  	 *
55  	 * @since 2.3
56  	 */
57  	@Parameter(defaultValue = "${project.build.directory}/scss-lint.xml", readonly = true)
58  	private File outputFile;
59  
60  	/**
61  	 * scss-lint exit codes and messages.
62  	 */
63  	public enum ExitCode {
64  		/** "No lints were found" exitcode. */
65  		CODE_0(0, "No lints were found"),
66  		/** "One or more warnings were reported" exitcode. */
67  		CODE_1(1, "Lints with a severity of 'warning' were reported (no errors)"),
68  		/** "One or more errors were reported" exitcode. */
69  		CODE_2(2, "One or more errors were reported (and any number of warnings)"),
70  		/** "Command line usage error" exitcode. */
71  		CODE_64(64, "Command line usage error (invalid flag, etc.)"),
72  		/** "" exitcode. */
73  		CODE_66(66, "Input file did not exist or was not readable"),
74  		/** "Input file did not exist or was not readable" exitcode. */
75  		CODE_70(70, "Internal software error"),
76  		/** "Internal software error" exitcode. */
77  		CODE_78(78, "Configuration error");
78  		/** "Configuration error" exitcode. */
79  
80  		private static final HashMap<Integer, ExitCode> LOOKUP = new HashMap<>();
81  
82  		static {
83  			for (final ExitCode c : EnumSet.allOf(ExitCode.class)) {
84  				LOOKUP.put(c.code, c);
85  			}
86  		}
87  
88  		private final String msg;
89  		private final int code;
90  
91  		ExitCode(final int code, final String msg) {
92  			this.code = code;
93  			this.msg = msg;
94  		}
95  
96  		String msg() {
97  			return msg;
98  		}
99  
100 		int code() {
101 			return code;
102 		}
103 
104 		static ExitCode getExitCode(final int code) {
105 			return LOOKUP.get(code);
106 		}
107 
108 		/**
109 		 * Returns a string representation of this {@code ExitCode}.
110 		 *
111 		 * @return code and message concatanated
112 		 */
113 		@Override
114 		public String toString() {
115 			return code + ": " + msg;
116 		}
117 	};
118 
119 	/**
120 	 * Execute the lint script.
121 	 *
122 	 * @see org.apache.maven.plugin.Mojo#execute()
123 	 * @throws MojoExecutionException when the execution of the plugin
124 	 *         errored
125 	 * @throws MojoFailureException when the Sass compilation fails
126 	 *
127 	 */
128 	@Override
129 	public void execute() throws MojoExecutionException, MojoFailureException {
130 		if (this.isSkip()) {
131 			return;
132 		}
133 		final Log log = this.getLog();
134 
135 		Set<String> sourceDirs = this.getSourceDirs();
136 		if (sourceDirs.isEmpty()) {
137 			return;
138 		}
139 		// create directory
140 		this.outputFile.getParentFile().mkdirs();
141 
142 		log.info("Linting Sass sources in: " + this.getSassSourceDirectory());
143 
144 		final StringBuilder sassScript = new StringBuilder();
145 		this.buildBasicSassScript(sassScript);
146 
147 		log.debug("scss-lint ruby script:\n" + sassScript);
148 
149 		System.setProperty("org.jruby.embed.localcontext.scope", "threadsafe");
150 		final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
151 		final ScriptEngine jruby = scriptEngineManager.getEngineByName("jruby");
152 		ScriptContext context = jruby.getContext();
153 
154 		ArrayList<String> argv = new ArrayList<>();
155 		argv.add("--format=Checkstyle");
156 		argv.add("--no-color");
157 		argv.add("-o" + this.outputFile);
158 		argv.addAll(sourceDirs);
159 		context.setAttribute(ScriptEngine.ARGV,
160 				argv.toArray(new String[argv.size()]),
161 				ScriptContext.GLOBAL_SCOPE);
162 		try {
163 			log.info("Reporting scss lint in: " + this.outputFile.getAbsolutePath());
164 			ExitCode result = ExitCode.getExitCode(
165 			        Ints.checkedCast((Long) jruby.eval(sassScript.toString(),
166 			                             context)));
167 			log.debug("scss-lint result: " + result.toString());
168 			switch (result) {
169 				case CODE_0:
170 					log.info(result.msg());
171 					break;
172 				case CODE_1:
173 					log.warn(result.msg());
174 					break;
175 				case CODE_2:
176 					log.error(result.toString());
177 					if (this.failOnError) {
178 						throw new MojoFailureException(result.toString());
179 					}
180 					break;
181 				// CHECKSTYLE:OFF:FallThrough
182 				case CODE_64:
183 				// fall through
184 				case CODE_66:
185 				// fall through
186 				case CODE_70:
187 				// fall through
188 				case CODE_78:
189 				// fall through
190 				default:
191 					log.error(result.toString());
192 					throw new MojoExecutionException(result.toString());
193 				// CHECKSTYLE:ON
194 			}
195 		} catch (final ScriptException e) {
196 			throw new MojoExecutionException(
197 					"Failed to execute scss-lint Ruby script:\n" + sassScript, e);
198 		}
199 	}
200 
201 	/**
202 	 * {@inheritDoc}
203 	 */
204 	@Override
205 	protected void buildBasicSassScript(final StringBuilder sassScript)
206 			throws MojoExecutionException {
207 		final Log log = this.getLog();
208 		// build up script
209 		sassScript.append("require 'scss_lint'\n");
210 		sassScript.append("require 'scss_lint/cli'\n");
211 		sassScript.append("require 'scss_lint_reporter_checkstyle'\n");
212 
213 		if (log.isDebugEnabled()) {
214 			// make ruby give use some debugging info when requested
215 			sassScript.append("require 'pp'\n");
216 			sassScript.append("puts 'parameters: '\n");
217 			sassScript.append("pp ARGV\n");
218 		}
219 		sassScript.append("logger = SCSSLint::Logger.new(STDOUT)\n");
220 		sassScript.append("SCSSLint::CLI.new(logger).run(ARGV)\n");
221 	}
222 
223 	/**
224 	 * Get the names of the sources.
225 	 * @return a set of String
226 	 */
227 	private Set<String> getSourceDirs() {
228 		Set<String> dirs = new HashSet<>();
229 		final List<Resource> resourceList = this.getResources();
230 		if (resourceList.isEmpty()) {
231 			dirs.add(getSassSourceDirectory().getPath());
232 		}
233 		for (final Resource source : resourceList) {
234 			dirs.addAll(source.getDirectoriesAndDestinations(this.getLog()).keySet());
235 		}
236 		return dirs;
237 	}
238 }