Skip to content

Commit 976811b

Browse files
committed
Add -lib option to nextflow lint
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent 295f173 commit 976811b

File tree

12 files changed

+280
-87
lines changed

12 files changed

+280
-87
lines changed

docs/reference/cli.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,11 +915,17 @@ The `lint` command parses and analyzes the given Nextflow scripts and config fil
915915
**Options**
916916

917917
`-exclude`
918-
: File pattern to exclude from linting. Can be specified multiple times (default: `.git, .nf-test, work`).
918+
: File pattern to exclude from linting (default: `.git, .lineage, .nextflow, .nf-test, nf-test.config, work`).
919+
: Can be specified multiple times.
919920

920921
`-format`
921922
: Format scripts and config files that have no errors.
922923

924+
`-lib`
925+
: :::{versionadded} 26.04.0
926+
:::
927+
: Path to lib directory (default: `'./lib'`).
928+
923929
`-o, -output`
924930
: Output mode for reporting errors: `full`, `extended`, `concise`, `json`, `markdown` (default: `full`).
925931

modules/nextflow/src/main/groovy/nextflow/Session.groovy

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ import nextflow.trace.event.TaskEvent
8181
import nextflow.trace.event.WorkflowOutputEvent
8282
import nextflow.trace.event.WorkflowPublishEvent
8383
import nextflow.util.Barrier
84-
import nextflow.util.ConfigHelper
84+
import nextflow.util.ClassLoaderFactory
8585
import nextflow.util.Duration
8686
import nextflow.util.HistoryFile
8787
import nextflow.util.LoggerHelper
@@ -601,21 +601,8 @@ class Session implements ISession {
601601
ScriptBinding getBinding() { binding }
602602

603603
@Memoized
604-
ClassLoader getClassLoader() { getClassLoader0() }
605-
606-
@PackageScope
607-
ClassLoader getClassLoader0() {
608-
// extend the class-loader if required
609-
final gcl = new GroovyClassLoader()
610-
final libraries = ConfigHelper.resolveClassPaths(getLibDir())
611-
612-
for( Path lib : libraries ) {
613-
def path = lib.complete()
614-
log.debug "Adding to the classpath library: ${path}"
615-
gcl.addClasspath(path.toString())
616-
}
617-
618-
return gcl
604+
ClassLoader getClassLoader() {
605+
ClassLoaderFactory.create(getLibDir())
619606
}
620607

621608
Barrier getBarrier() { monitorsBarrier }

modules/nextflow/src/main/groovy/nextflow/cli/CmdLint.groovy

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import nextflow.script.formatter.ScriptFormattingVisitor
4040
import nextflow.script.parser.v2.ErrorListener
4141
import nextflow.script.parser.v2.ErrorSummary
4242
import nextflow.script.parser.v2.StandardErrorListener
43+
import nextflow.util.ClassLoaderFactory
4344
import nextflow.util.PathUtils
4445
import org.codehaus.groovy.control.SourceUnit
4546
import org.codehaus.groovy.control.messages.SyntaxErrorMessage
@@ -62,7 +63,13 @@ class CmdLint extends CmdBase {
6263
names = ['-exclude'],
6364
description = 'File pattern to exclude from error checking (can be specified multiple times)'
6465
)
65-
List<String> excludePatterns = ['.git', '.lineage', '.nf-test', '.nextflow', 'work', 'nf-test.config']
66+
List<String> excludePatterns = ['.git', '.lineage', '.nextflow', '.nf-test', 'nf-test.config', 'work']
67+
68+
@Parameter(
69+
names = ['-lib'],
70+
description = 'Path to lib directory (default: ./lib)'
71+
)
72+
String libDir
6673

6774
@Parameter(
6875
names = ['-o', '-output'],
@@ -121,7 +128,10 @@ class CmdLint extends CmdBase {
121128
if( !spaces && !tabs )
122129
spaces = 4
123130

124-
scriptParser = new ScriptParser()
131+
final libPath = Path.of(libDir ?: 'lib')
132+
final classLoader = ClassLoaderFactory.create([ libPath ])
133+
134+
scriptParser = new ScriptParser(classLoader)
125135
configParser = new ConfigParser()
126136
errorListener = outputMode == 'json'
127137
? new JsonErrorListener()
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright 2013-2025, Seqera Labs
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+
17+
package nextflow.util
18+
19+
import java.nio.file.Path
20+
21+
import groovy.transform.CompileStatic
22+
import groovy.util.logging.Slf4j
23+
/**
24+
* Helper methods to create class loaders with
25+
* additional classpaths.
26+
*
27+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
28+
*/
29+
@Slf4j
30+
@CompileStatic
31+
class ClassLoaderFactory {
32+
33+
/**
34+
* Create a class loader with the given directories
35+
* added to the classpath.
36+
*
37+
* @param dirs
38+
*/
39+
static GroovyClassLoader create(List<Path> dirs) {
40+
final gcl = new GroovyClassLoader()
41+
final libraries = resolveClassPaths(dirs)
42+
for( final lib : libraries ) {
43+
gcl.addClasspath(lib.complete().toString())
44+
}
45+
return gcl
46+
}
47+
48+
/**
49+
* Given a list of directories, look for the files ending with
50+
* the '.jar' extension and return a list containing the original
51+
* directories and the JAR paths.
52+
*
53+
* @param dirs
54+
*/
55+
static List<Path> resolveClassPaths(List<Path> dirs) {
56+
57+
List<Path> result = []
58+
59+
if( !dirs )
60+
return result
61+
62+
for( final path : dirs ) {
63+
if( path.isFile() && path.name.endsWith('.jar') ) {
64+
result << path
65+
}
66+
else if( path.isDirectory() ) {
67+
result << path
68+
path.eachFileMatch( ~/.+\.jar$/ ) {
69+
if( it.isFile() )
70+
result << it
71+
}
72+
}
73+
}
74+
75+
return result
76+
}
77+
78+
}

modules/nextflow/src/main/groovy/nextflow/util/ConfigHelper.groovy

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package nextflow.util
1818

19-
import java.nio.file.Path
20-
2119
import groovy.json.JsonOutput
2220
import groovy.transform.CompileStatic
2321
import groovy.transform.PackageScope
@@ -69,32 +67,6 @@ class ConfigHelper {
6967
return obj
7068
}
7169

72-
/**
73-
* Given a list of paths looks for the files ending with the extension '.jar' and return
74-
* a list containing the original directories, plus the JARs paths
75-
*
76-
* @param dirs
77-
* @return
78-
*/
79-
static List<Path> resolveClassPaths( List<Path> dirs ) {
80-
81-
List<Path> result = []
82-
if( !dirs )
83-
return result
84-
85-
for( Path path : dirs ) {
86-
if( path.isFile() && path.name.endsWith('.jar') ) {
87-
result << path
88-
}
89-
else if( path.isDirectory() ) {
90-
result << path
91-
path.eachFileMatch( ~/.+\.jar$/ ) { if(it.isFile()) result << it }
92-
}
93-
}
94-
95-
return result
96-
}
97-
9870
static private final String TAB = ' '
9971

10072
static private void canonicalFormat(StringBuilder writer, ConfigObject object, int level, boolean sort, List stack) {

modules/nextflow/src/main/groovy/nextflow/util/RemoteSession.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class RemoteSession implements Serializable, Closeable {
5454
private transient List<Path> localPaths = deserialiseClasspath()
5555

5656
@Lazy
57-
private transient List<Path> resolvedClasspath = ConfigHelper.resolveClassPaths(localPaths)
57+
private transient List<Path> resolvedClasspath = ClassLoaderFactory.resolveClassPaths(localPaths)
5858

5959
protected RemoteSession() { }
6060

modules/nextflow/src/test/groovy/nextflow/cli/CmdLintTest.groovy

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,39 @@ class CmdLintTest extends Specification {
8181
dir?.deleteDir()
8282
}
8383

84+
def 'should add lib directory to class loader' () {
85+
86+
given:
87+
def dir = Files.createTempDirectory('test')
88+
89+
dir.resolve('main.nf').text = '''\
90+
println Utils.hello()
91+
'''
92+
93+
dir.resolve('lib').mkdir()
94+
dir.resolve('lib/Utils.groovy').text = '''\
95+
class Utils {
96+
97+
String hello() {
98+
return 'Hello!'
99+
}
100+
}
101+
'''
102+
103+
when:
104+
def cmd = new CmdLint()
105+
cmd.args = [dir.toString()]
106+
cmd.libDir = dir.resolve('lib').toString()
107+
cmd.launcher = Mock(Launcher) {
108+
getOptions() >> Mock(CliOptions)
109+
}
110+
cmd.run()
111+
112+
then:
113+
noExceptionThrown()
114+
115+
cleanup:
116+
dir?.deleteDir()
117+
}
118+
84119
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
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+
17+
package nextflow.util
18+
19+
import java.nio.file.Files
20+
import java.nio.file.Path
21+
22+
import spock.lang.Specification
23+
/**
24+
*
25+
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
26+
*/
27+
class ClassLoaderFactoryTest extends Specification {
28+
29+
def testResolveClasspaths() {
30+
31+
given:
32+
def path1 = Files.createTempDirectory('path1')
33+
path1.resolve('file1').text = 'File 1'
34+
path1.resolve('file2.jar').text = 'File 2'
35+
path1.resolve('dir').mkdir()
36+
path1.resolve('dir/file3').text = 'File 3'
37+
path1.resolve('dir/file4').text = 'File 4'
38+
39+
def path2 = Files.createTempDirectory('path2')
40+
path2.resolve('file5').text = 'File 5'
41+
path2.resolve('file6.jar').text = 'File 6'
42+
43+
def path3 = Path.of('/some/file')
44+
45+
when:
46+
def list = ClassLoaderFactory.resolveClassPaths([path1, path2, path3])
47+
then:
48+
list.size() == 4
49+
list.contains( path1 )
50+
list.contains( path1.resolve('file2.jar') )
51+
list.contains( path2 )
52+
list.contains( path2.resolve('file6.jar') )
53+
54+
cleanup:
55+
path1?.deleteDir()
56+
path2?.deleteDir()
57+
}
58+
59+
}

modules/nextflow/src/test/groovy/nextflow/util/ConfigHelperTest.groovy

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@
1616

1717
package nextflow.util
1818

19-
import java.nio.file.Files
20-
import java.nio.file.Paths
21-
2219
import nextflow.config.ConfigClosurePlaceholder
2320
import spock.lang.Specification
2421
import spock.lang.Unroll
@@ -47,37 +44,6 @@ class ConfigHelperTest extends Specification {
4744

4845
}
4946

50-
def testResolveClasspaths() {
51-
52-
given:
53-
def path1 = Files.createTempDirectory('path1')
54-
path1.resolve('file1').text = 'File 1'
55-
path1.resolve('file2.jar').text = 'File 2'
56-
path1.resolve('dir').mkdir()
57-
path1.resolve('dir/file3').text = 'File 3'
58-
path1.resolve('dir/file4').text = 'File 4'
59-
60-
def path2 = Files.createTempDirectory('path2')
61-
path2.resolve('file5').text = 'File 5'
62-
path2.resolve('file6.jar').text = 'File 6'
63-
64-
def path3 = Paths.get('/some/file')
65-
66-
when:
67-
def list = ConfigHelper.resolveClassPaths([path1, path2, path3])
68-
then:
69-
list.size() == 4
70-
list.contains( path1 )
71-
list.contains( path1.resolve('file2.jar') )
72-
list.contains( path2 )
73-
list.contains( path2.resolve('file6.jar') )
74-
75-
cleanup:
76-
path1?.deleteDir()
77-
path2?.deleteDir()
78-
79-
}
80-
8147
def 'should render config using properties notation' () {
8248

8349
given:

0 commit comments

Comments
 (0)