Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lint and format commands #5908

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/cli/CmdFormat.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2013-2024, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.cli

import com.beust.jcommander.Parameter
import com.beust.jcommander.Parameters
import groovy.io.FileType
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import nextflow.config.control.ConfigParser
import nextflow.config.formatter.ConfigFormattingVisitor
import nextflow.exception.AbortOperationException
import nextflow.script.control.ScriptParser
import nextflow.script.formatter.FormattingOptions
import nextflow.script.formatter.ScriptFormattingVisitor
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.messages.SyntaxErrorMessage
/**
* CLI sub-command FORMAT
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
@Slf4j
@CompileStatic
@Parameters(commandDescription = "Format Nextflow scripts and config files")
class CmdFormat extends CmdBase {

@Parameter(description = 'List of paths to format')
List<String> args = []

@Parameter(names=['-spaces'], description = 'Number of spaces to indent')
int spaces

@Parameter(names = ['-tabs'], description = 'Indent with tabs')
Boolean tabs

@Parameter(names = ['-harshil-alignment'], description = 'Use Harshil alignment')
Boolean harhsilAlignment

private ScriptParser scriptParser

private ConfigParser configParser

private FormattingOptions formattingOptions

@Override
String getName() { 'format' }

@Override
void run() {
if( !args )
throw new AbortOperationException("No input files specified")

if( spaces && tabs )
throw new AbortOperationException("Cannot specify both `-spaces` and `-tabs`")

if( !spaces && !tabs )
spaces = 4

scriptParser = new ScriptParser()
configParser = new ConfigParser()
formattingOptions = new FormattingOptions(spaces, !tabs, harhsilAlignment, false)

for( final arg : args ) {
final file = new File(arg)
if( file.isFile() ) {
format(file)
continue
}

file.eachFileRecurse(FileType.FILES, this.&format)
}
}

void format(File file) {
final name = file.getName()
if( name.endsWith('.nf') )
formatScript(file)
else if( name.endsWith('.config') )
formatConfig(file)
}

private void formatScript(File file) {
final source = scriptParser.parse(file)
if( source.getErrorCollector().hasErrors() ) {
printErrors(source)
}
else {
log.debug "Formatting script ${file}"
final formatter = new ScriptFormattingVisitor(source, formattingOptions)
formatter.visit()
file.text = formatter.toString()
}
}

private void formatConfig(File file) {
final source = configParser.parse(file)
if( source.getErrorCollector().hasErrors() ) {
printErrors(source)
}
else {
log.debug "Formatting config ${file}"
final formatter = new ConfigFormattingVisitor(source, formattingOptions)
formatter.visit()
file.text = formatter.toString()
}
}

private void printErrors(SourceUnit source) {
final errorMessages = source.getErrorCollector().getErrors()
System.err.println()
for( final message : errorMessages ) {
if( message instanceof SyntaxErrorMessage ) {
final cause = message.getCause()
System.err.println "${source.getName()} at line ${cause.getStartLine()}, column ${cause.getStartColumn()}: ${cause.getOriginalMessage()}"
}
}
}
}
103 changes: 103 additions & 0 deletions modules/nextflow/src/main/groovy/nextflow/cli/CmdLint.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright 2013-2024, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.cli

import com.beust.jcommander.Parameter
import com.beust.jcommander.Parameters
import groovy.io.FileType
import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import nextflow.config.control.ConfigParser
import nextflow.exception.AbortOperationException
import nextflow.script.control.Compiler
import nextflow.script.control.ScriptParser
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.messages.SyntaxErrorMessage
/**
* CLI sub-command LINT
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
@Slf4j
@CompileStatic
@Parameters(commandDescription = "Check Nextflow scripts and config files for errors")
class CmdLint extends CmdBase {

@Parameter(description = 'List of paths to lint')
List<String> args = []

private ScriptParser scriptParser

private ConfigParser configParser

@Override
String getName() { 'lint' }

@Override
void run() {
if( !args )
throw new AbortOperationException("No input files specified")

scriptParser = new ScriptParser()
configParser = new ConfigParser()

for( final arg : args ) {
final file = new File(arg)
if( file.isFile() ) {
parse(file)
continue
}

file.eachFileRecurse(FileType.FILES, this.&parse)
}

scriptParser.analyze()
configParser.analyze()
checkErrors(scriptParser.compiler())
checkErrors(configParser.compiler())
}

void parse(File file) {
final name = file.getName()
if( name.endsWith('.nf') )
scriptParser.parse(file)
else if( name.endsWith('.config') )
configParser.parse(file)
}

private void checkErrors(Compiler compiler) {
compiler.getSources()
.values()
.stream()
.sorted(Comparator.comparing((SourceUnit source) -> source.getSource().getURI()))
.forEach((source) -> {
if( source.getErrorCollector().hasErrors() )
printErrors(source)
})
}

private void printErrors(SourceUnit source) {
final errorMessages = source.getErrorCollector().getErrors()
System.err.println()
for( final message : errorMessages ) {
if( message instanceof SyntaxErrorMessage ) {
final cause = message.getCause()
System.err.println "${source.getName()} at line ${cause.getStartLine()}, column ${cause.getStartColumn()}: ${cause.getOriginalMessage()}"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ class Launcher {
new CmdHelp(),
new CmdSelfUpdate(),
new CmdPlugin(),
new CmdInspect()
new CmdInspect(),
new CmdLint(),
new CmdFormat()
]

if(SecretsLoader.isEnabled())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2024-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package nextflow.config.control;

import java.io.File;
import java.util.Collections;

import groovy.lang.GroovyClassLoader;
import nextflow.config.parser.ConfigParserPluginFactory;
import nextflow.script.control.Compiler;
import nextflow.script.control.LazyErrorCollector;
import nextflow.script.types.Types;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.control.messages.WarningMessage;

/**
* Parse and analyze config files without compiling to Groovy.
*
* @author Ben Sherman <bentshermann@gmail.com>
*/
public class ConfigParser {

private Compiler compiler;

public ConfigParser() {
var config = getConfig();
var classLoader = new GroovyClassLoader(ClassLoader.getSystemClassLoader().getParent(), config, true);
compiler = new Compiler(config, classLoader);
}

public Compiler compiler() {
return compiler;
}

public SourceUnit parse(File file) {
var source = getSource(file);
compiler.addSource(source);
compiler.compile(source);
return source;
}

public void analyze() {
for( var source : compiler.getSources().values() ) {
var includeResolver = new ResolveIncludeVisitor(source, compiler, Collections.emptySet());
includeResolver.visit();
for( var error : includeResolver.getErrors() )
source.getErrorCollector().addErrorAndContinue(error);
new ConfigResolveVisitor(source, compiler.compilationUnit()).visit();
}
}

private SourceUnit getSource(File file) {
return new SourceUnit(
file,
compiler.compilationUnit().getConfiguration(),
compiler.compilationUnit().getClassLoader(),
new LazyErrorCollector(compiler.compilationUnit().getConfiguration()));
}

private static CompilerConfiguration getConfig() {
var config = new CompilerConfiguration();
config.setPluginFactory(new ConfigParserPluginFactory());
config.setWarningLevel(WarningMessage.POSSIBLE_ERRORS);
return config;
}

}
Loading