[ 
https://issues.apache.org/jira/browse/GROOVY-12411?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18124543#comment-18124543
 ] 

ASF GitHub Bot commented on GROOVY-12411:
-----------------------------------------

github-advanced-security[bot] commented on code in PR #2936:
URL: https://github.com/apache/groovy/pull/2936#discussion_r4208232830


##########
subprojects/groovy-lsp/src/main/java/org/apache/groovy/lsp/internal/compile/GroovyCompiler.java:
##########
@@ -0,0 +1,423 @@
+/*
+ *  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.groovy.lsp.internal.compile;
+
+import groovy.lang.GroovyClassLoader;
+import org.apache.groovy.lsp.internal.intelligence.StaticTypeCheckingPass;
+import org.apache.groovy.lsp.internal.util.Uris;
+import org.apache.groovy.lsp.internal.workspace.TextDocument;
+import org.codehaus.groovy.control.CompilationFailedException;
+import org.codehaus.groovy.control.CompilationUnit;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.SourceUnit;
+import org.codehaus.groovy.tools.javac.JavaAwareCompilationUnit;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * Compiles Groovy sources through semantic analysis (no class files) so
+ * the language server can offer diagnostics and navigation on a partial
+ * AST. Errors are collected rather than aborting the compile.
+ */
+public final class GroovyCompiler implements AutoCloseable {
+
+    private static final String ASTTEST_ENABLE = "groovy.asttest.enable";
+    private static final String GRAPE_ENABLE = "groovy.grape.enable";
+    private static final Object PROCESS_FLAGS = new Object();
+
+    private GroovyClassLoader groovyLoader;
+    private URLClassLoader classpathLoader;
+    private LoaderKey loaderKey;
+    private Path javacOutput;
+
+    /**
+     * Compiles {@code openDocuments} together with extra files from disk
+     * that are not already open. Open buffers overlay the filesystem.
+     *
+     * @param openDocuments open editor buffers
+     * @param extraFiles additional Groovy files
+     * @param settings compiler settings
+     * @param parentLoader parent class loader
+     * @return an immutable snapshot
+     */
+    public CompilationSnapshot compile(final Collection<TextDocument> 
openDocuments, final Collection<Path> extraFiles,
+                                       final CompilerSettings settings, final 
ClassLoader parentLoader) {
+        synchronized (this) {
+            CompilerConfiguration configuration = settings.toConfiguration();
+            return withProcessFlags(settings, () ->
+                    compileUnit(openDocuments, extraFiles, settings, 
parentLoader, configuration));
+        }
+    }
+
+    private CompilationSnapshot compileUnit(final Collection<TextDocument> 
openDocuments, final Collection<Path> extraFiles,
+                                            final CompilerSettings settings, 
final ClassLoader parentLoader,
+                                            final CompilerConfiguration 
configuration) {
+        Map<URI, Integer> versions = new LinkedHashMap<>();
+        Map<URI, String> texts = new LinkedHashMap<>();
+        Map<URI, String> javaOverlays = new LinkedHashMap<>();
+        List<File> javaFiles = collectJavaSources(openDocuments, extraFiles, 
versions, texts, javaOverlays);
+        LspJavaCompiler javaCompiler = javaFiles.isEmpty() ? null : new 
LspJavaCompiler(javaOverlays);
+        CompilationUnit unit = compilationUnit(settings, parentLoader, 
configuration, javaCompiler, !javaFiles.isEmpty());
+        addOpenDocuments(unit, openDocuments, versions, texts);
+        addExtraFiles(unit, extraFiles, versions, texts);
+        if (unit instanceof JavaAwareCompilationUnit javaUnit && 
!javaFiles.isEmpty()) {
+            javaUnit.addSources(javaFiles.toArray(File[]::new));
+        }
+
+        try {
+            unit.compile(settings.getThroughPhase());
+        } catch (CompilationFailedException ignored) {
+            // keep the partial AST and collected messages
+        }
+        StaticTypeCheckingPass.apply(unit);
+
+        Map<URI, CompiledDocument> compiled = CompilationSnapshot.newMap();
+        var sourceIterator = unit.iterator();
+        while (sourceIterator.hasNext()) {
+            addCompiled(compiled, versions, texts, sourceIterator.next());
+        }
+        Map<URI, List<JavaMessage>> javaMessages = javaCompiler == null ? 
Map.of() : javaCompiler.messages();
+        for (Map.Entry<URI, String> entry : texts.entrySet()) {
+            compiled.computeIfAbsent(entry.getKey(), uri -> new 
CompiledDocument(uri,
+                    versions.getOrDefault(uri, -1), entry.getValue(), null, 
null, null));
+        }
+        for (Map.Entry<URI, List<JavaMessage>> entry : 
javaMessages.entrySet()) {
+            compiled.compute(entry.getKey(), (uri, current) -> {
+                CompiledDocument base = current == null
+                        ? new CompiledDocument(uri, versions.getOrDefault(uri, 
-1),
+                        texts.getOrDefault(uri, ""), null, null, null) : 
current;
+                return base.withJavaMessages(entry.getValue());
+            });
+        }
+        JavaSymbolIndex javaSymbols = javaCompiler == null ? 
JavaSymbolIndex.EMPTY : javaCompiler.index();
+        return new CompilationSnapshot(compiled, javaSymbols);
+    }
+
+    private CompilationUnit compilationUnit(final CompilerSettings settings, 
final ClassLoader parentLoader,
+                                            final CompilerConfiguration 
configuration, final LspJavaCompiler javaCompiler,
+                                            final boolean joint) {
+        if (!joint || javaCompiler == null) {
+            return new CompilationUnit(configuration, null, 
loaderFor(settings, parentLoader, configuration));
+        }
+        Map<String, Object> jointOptions = new HashMap<>();
+        jointOptions.put(CompilerConfiguration.MEM_STUB, Boolean.TRUE);
+        configuration.setJointCompilationOptions(jointOptions);
+        GroovyClassLoader loader = loaderFor(settings, parentLoader, 
configuration);
+        if (!prepareJavacOutput(configuration)) {
+            return new CompilationUnit(configuration, null, loader);
+        }
+        JavaAwareCompilationUnit unit = new 
JavaAwareCompilationUnit(configuration, loader);
+        unit.setCompilerFactory(javaCompiler);
+        return unit;
+    }
+
+    private synchronized boolean prepareJavacOutput(final 
CompilerConfiguration configuration) {
+        deleteJavacOutput();
+        try {
+            javacOutput = Files.createTempDirectory("groovy-lsp-javac-");

Review Comment:
   ## SonarCloud / Temporary files should not be created in publicly writable 
directories
   
   <!--SONAR_ISSUE_KEY:AaEWyq1fI3kxgnqHLHJM-->Make sure publicly writable 
directories are used safely here. <p>See more on <a 
href="https://sonarcloud.io/project/issues?id=apache_groovy&issues=AaEWyq1fI3kxgnqHLHJM&open=AaEWyq1fI3kxgnqHLHJM&pullRequest=2936";>SonarQube
 Cloud</a></p>
   
   [Show more 
details](https://github.com/apache/groovy/security/code-scanning/283)





> Add a first-party Language Server Protocol implementation (groovy-lsp)
> ----------------------------------------------------------------------
>
>                 Key: GROOVY-12411
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12411
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> Apache Groovy needs a language server that uses the compiler in this 
> repository. Community servers compile against a published Groovy jar, so 
> parser and type-system changes reach an editor only after that jar is 
> released. This change adds an optional {{groovy-lsp}} module and a 
> {{groovylsp}} command. Editors, and later AI tools, share that language core. 
> It is not a full IDE, and it does not replace Groovy-Eclipse or IntelliJ.
> h2. Why this shape
> The server compiles the open buffers and answers Language Server Protocol 
> (LSP) requests from that snapshot. A language change in this tree shows up in 
> the server without forking a community project. The server refuses edits that 
> would guess: it will not rename an unbound dynamic call, and it will not 
> rewrite every token that happens to spell the same name.
> There is no public extension SPI in this first cut. No ServiceLoader hook, no 
> OSGi bundle, and no second hover or completion API. Static type checking of 
> annotated members is core behavior, not a plugin. A later {{groovy-mcp}} 
> wrapper may call the in-process engine. That wrapper is not part of this 
> change.
> h2. What lands
> * Optional subproject {{groovy-lsp}}. It is not part of {{groovy-all}}.
> * {{groovylsp}} and {{groovylsp.bat}} on the binary distribution. The default 
> transport is stdio. {{\-\-socket}} accepts one TCP client. {{\-\-help}} and 
> {{\-\-version}} print and exit.
> * LSP 3.18, carried by Eclipse LSP4J 1.0. Fields from a newer draft that the 
> server does not understand are ignored. A capability is advertised only when 
> it is implemented.
> * LSP4J is an EPL 2.0 Category B binary. Its sources are not copied into 
> Groovy.
> * Open Groovy buffers compile through semantic analysis. The server does not 
> generate Groovy classes.
> * {{.java}} files in the workspace are joint-compiled with 
> {{JavaAwareCompilationUnit}} ({{memStub}}) and {{javax.tools}} javac, with 
> annotation processing off.
> * After semantic analysis, a core pass type-checks members annotated 
> {{@TypeChecked}} or {{@CompileStatic}}. It skips {{@CompileDynamic}} and 
> {{TypeCheckingMode.SKIP}}, and it does not force static checking on ordinary 
> dynamic Groovy.
> * {{GroovyLanguageEngine}} is the in-process API: file paths, 0-based UTF-16 
> positions, and short source snippets. Rename returns edits and does not write 
> the files.
> * {{LocatedMessage.toDiagnostic()}} and {{write()}} no longer throw 
> {{NullPointerException}} when the parse context is null. They report the 
> message with no line or column.
> * {{ASTNode}} column javadoc now matches the compiler: columns are 1-based 
> Unicode code points, the end column is exclusive, and a missing position is 
> -1. Getters, setters, and stored values are unchanged. See GROOVY-12085.
> * User guide {{groovy-lsp.adoc}} and contributor map 
> {{subprojects/groovy-lsp/ARCHITECTURE.md}}.
> h2. What an editor gets
> Recognized sources are {{.groovy}}, {{.gvy}}, {{.gy}}, {{.gsh}}, or language 
> id {{groovy}}.
> * *Editing:* incremental sync; completion of keywords, snippets, members, 
> workspace types (with an import edit), and GDK members; Groovydoc filled in 
> when the client resolves a completion item; format, range format, and on-type 
> indent; organize imports, keeping star, static, and alias imports; 
> unused-import hints; code actions to add an import, add a package from the 
> path, implement abstract methods, insert an inferred type, create a missing 
> class stub, add {{@Override}}, and generate accessors, {{toString}}, 
> {{equals}} and {{hashCode}}, and a constructor.
> * *Navigation:* definition, type definition, implementation, declaration, and 
> references. References match the declaration (owner, name, and descriptor), 
> not the bare spelling. Call hierarchy and type hierarchy. Document symbols 
> and workspace symbols. A workspace-symbol query may be a case-insensitive 
> substring or CamelHumps, so {{ASF}} finds {{AbstractServerFactory}}. Folding, 
> selection ranges, document highlight, and document links on imports.
> * *Presentation:* push and pull diagnostics (compiler messages, unused 
> imports, static-type errors on annotated members, and javac messages from a 
> joint compile). Semantic tokens. Inlay hints for declaration types and for 
> parameter names. Code lenses that show a reference count.
> * *Workspace:* workspace folders, watched files, configuration, and progress 
> when the client asks for it. {{workspace/willRenameFiles}} updates one 
> top-level Groovy class whose name matches the file name without its 
> extension. It refuses a script, a file that declares several types, a class 
> name that does not match, and {{.java}}.
> Commands:
> * {{groovy.lsp.organizeImports}}
> * {{groovy.lsp.showReferences}}
> * {{groovy.lsp.gotoSuperMethod}} (nearest overridden method that has source; 
> skips {{Object}} and {{GroovyObject}})
> h2. Rename
> {{methodTarget}} is often empty on a dynamic call. A match by name and arity 
> is enough to display, and not enough to edit.
> ||Feature||When the compiler bound the call||When it did not||
> |Hover|The bound signature|One arity match on a resolved receiver, marked 
> {{_inferred_}}; otherwise the name only|
> |Definition|That method|Every arity match that has source. Never an arbitrary 
> first hit|
> |Rename and prepare-rename|Allowed|Refused|
> A class, method, field, parameter, or bound local is renamed by identity. 
> Renaming a class also renames the file when the class name matches the file 
> name without its extension. An inlay marked {{inferred}} is display only. It 
> does not insert text.
> h2. Groovy and Java in one workspace
> Joint compilation lets Groovy and Java sources in the workspace see each 
> other's types. Go to definition crosses the language boundary through 
> {{JavaSymbolIndex}}, and javac diagnostics are published the same way. javac 
> class files go to a temporary directory and are removed on the next compile. 
> This is not a Java language server. Java completion, Java formatting, and 
> Java-only rename stay with a Java language server. Eclipse JDT and 
> groovy-eclipse remain the stack for a full Java IDE. No source is copied from 
> the EPL server groovy-language-server-edt, and its identifier-scan rename is 
> not adopted.
> ||Question||groovy-lsp||Other Groovy language servers||
> |Which compiler|The compiler in this repository|A published Groovy jar, or 
> JDT plus groovy-eclipse|
> |Java IDE features|Joint type resolution and cross-language definition|Java 
> completion, formatting, and Java-only rename|
> |GDSL and dependency download|Not included|groovyls adds Jenkins GDSL and 
> dependency download|
> |Unbound-call rename|Refused|An identifier-scan fallback exists in 
> groovy-language-server-edt|
> h2. Configuration and safety
> On {{initialized}}, if the client advertised {{workspace/configuration}}, the 
> server asks for the {{groovy}} section without blocking the JSON-RPC reader. 
> The client can also push {{workspace/didChangeConfiguration}}.
> ||Key||Meaning||
> |classpath|Extra jars and directories. The server does not run Gradle or 
> Maven|
> |sourcePaths|Extra source roots, relative to a workspace folder when not 
> absolute|
> |grapeEnabled|Allow {{@Grab}} while compiling. Default is off|
> |astTestEnabled|Allow {{@ASTTest}} while compiling. Default is off|
> Any other key in the {{groovy}} section is stored as text. The server does 
> not interpret it and does not download anything for it. Changing it builds a 
> new class loader and recompiles.
> When classpath and source paths are empty, the server looks for 
> {{src/main/groovy}}, {{src/test/groovy}}, {{src/main/java}}, and 
> {{src/test/java}}, then {{src}}, then the workspace folder itself, plus jars 
> directly under {{lib}} or {{libs}}.
> Diagnostics are limited to the file that owns them. Unchanged buffers are not 
> compiled again. {{@Grab}} and {{@ASTTest}} stay off unless the client turns 
> them on, because both can run code at compile time.
> h2. Out of scope
> * A full Java language server.
> * GDSL and Jenkins pipeline descriptors.
> * Invoking Gradle or Maven, including the Tooling API.
> * A debugger or a test runner.
> * Renaming by identifier text.
> * Name-span fields on {{AnnotatedNode}}. Identifier ranges stay inside 
> groovy-lsp.
> * A public plugin SPI.
> h2. Positions
> This is not an LSP break. Compiler columns are already 1-based Unicode code 
> points (GROOVY-12085). LSP positions stay 0-based in the encoding chosen at 
> initialize: {{utf-16}} unless the client offers {{utf-8}} or {{utf-32}}. The 
> server converts both ways, including characters outside the Basic 
> Multilingual Plane. Existing clients keep sending positions as they do today.
> h2. Running
> {code}
> groovylsp
> groovylsp --stdio
> groovylsp --socket PORT
> groovylsp --help
> groovylsp --version
> {code}
> An editor should start that process and speak JSON-RPC on stdin and stdout. 
> While the server is on stdio, standard output is redirected to standard error 
> so a print from Groovy code cannot corrupt the protocol stream.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to