mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 04:43:26 +03:00
Implement private modules (#7840)
Adds the ability to declare a module as *private*. Modifies the parser to add the `private` keyword as a reserved keyword. All the checks for private modules are implemented as an independent *Compiler pass*. No checks are done at runtime. # Important Notes - Introduces new keyword - `private` - a reserved keyword. - Modules that have `private` keyword as the first statement are declared as *private* (Project private) - Public module cannot have private submodules and vice versa. - This would require runtime access checks - See #7088 for the specification.
This commit is contained in:
parent
421e3f22a4
commit
c22928ecc2
@ -972,6 +972,7 @@
|
|||||||
- [Downloadable VSCode extension][7861]
|
- [Downloadable VSCode extension][7861]
|
||||||
- [New `project/status` route for reporting LS state][7801]
|
- [New `project/status` route for reporting LS state][7801]
|
||||||
- [Add Enso-specific assertions][7883])
|
- [Add Enso-specific assertions][7883])
|
||||||
|
- [Modules can be `private`][7840]
|
||||||
|
|
||||||
[3227]: https://github.com/enso-org/enso/pull/3227
|
[3227]: https://github.com/enso-org/enso/pull/3227
|
||||||
[3248]: https://github.com/enso-org/enso/pull/3248
|
[3248]: https://github.com/enso-org/enso/pull/3248
|
||||||
@ -1117,6 +1118,7 @@
|
|||||||
[7825]: https://github.com/enso-org/enso/pull/7825
|
[7825]: https://github.com/enso-org/enso/pull/7825
|
||||||
[7861]: https://github.com/enso-org/enso/pull/7861
|
[7861]: https://github.com/enso-org/enso/pull/7861
|
||||||
[7883]: https://github.com/enso-org/enso/pull/7883
|
[7883]: https://github.com/enso-org/enso/pull/7883
|
||||||
|
[7840]: https://github.com/enso-org/enso/pull/7840
|
||||||
|
|
||||||
# Enso 2.0.0-alpha.18 (2021-10-12)
|
# Enso 2.0.0-alpha.18 (2021-10-12)
|
||||||
|
|
||||||
|
@ -29,3 +29,5 @@ This specification is broken down into the following sections:
|
|||||||
those of suspended computations.
|
those of suspended computations.
|
||||||
- [**Modules:**](./modules.md) The semantics of Enso's modules.
|
- [**Modules:**](./modules.md) The semantics of Enso's modules.
|
||||||
- [**Scoping:**](./scoping.md) Enso's scoping and identifier resolution rules.
|
- [**Scoping:**](./scoping.md) Enso's scoping and identifier resolution rules.
|
||||||
|
- [**Encapsulation:**](./encapsulation.md) The semantics of Enso's encapsulation
|
||||||
|
system - access restriction.
|
||||||
|
178
docs/semantics/encapsulation.md
Normal file
178
docs/semantics/encapsulation.md
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
---
|
||||||
|
layout: developer-doc
|
||||||
|
title: Diagnostics
|
||||||
|
category: semantics
|
||||||
|
tags: [semantics, diagnostics, runtime]
|
||||||
|
order: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
# Encapsulation
|
||||||
|
|
||||||
|
_Encapsulation_ is the system of hiding certain internal **entities** (modules,
|
||||||
|
types, methods, constructors, fields) in one project/library from other
|
||||||
|
projects/libraries. This document is an excerpt from the discussion held at
|
||||||
|
https://github.com/orgs/enso-org/discussions/7088.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Be able to hide an entity on demand. By hiding, we mean that the entity cannot
|
||||||
|
be directly imported, and that it cannot be used via FQN.
|
||||||
|
- i.e. an entity shall be hidden both during compile time (project
|
||||||
|
compilation), and during runtime.
|
||||||
|
- Entity being hidden at runtime implies that it does not have any entry in
|
||||||
|
the Suggestion database, therefore, no entry in the Component browser.
|
||||||
|
- Be able to import all public symbols from a library with
|
||||||
|
`from Library import all`.
|
||||||
|
- Be able to import a selected set of symbols from a library with
|
||||||
|
`from Library import Symbol_1, Symbol_2, ...`.
|
||||||
|
- Import a public symbol directly with
|
||||||
|
`import Library.Public_Module.Public_Type`.
|
||||||
|
- Use a public symbol via FQN: `Library.Public_Module.Public_Type`.
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Let's introduce a `private` keyword. By prepending (syntax rules discussed
|
||||||
|
below) `private` keyword` to an entity, we declare it as **project private**. A
|
||||||
|
project-private entity is an entity that can be imported and used in the same
|
||||||
|
project, but cannot be imported nor used in different projects. Note that it is
|
||||||
|
not desirable to declare the entities as _module private_, as that would be too
|
||||||
|
restrictive, and would prevent library authors using the entity within the
|
||||||
|
project.
|
||||||
|
|
||||||
|
From now on, let's consider _project-private_ and _private_ synonymous, and
|
||||||
|
**public** as an entity that is not private.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
All the entities, except modules, shall be declared private by prepending them
|
||||||
|
with `private` keyword. Declaring a module as private shall be done be writing
|
||||||
|
the `private` keyword at the very beginning of the module, before all the import
|
||||||
|
statements, ignoring all the comments before. Fields cannot have `private`
|
||||||
|
keyword, only constructors. Types cannot have `private` keyword as well - only
|
||||||
|
methods and constructors.
|
||||||
|
|
||||||
|
## Semantics
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
|
||||||
|
Modules can be specified as private. Private modules cannot be imported from
|
||||||
|
other projects. Private modules can be imported from the same project.
|
||||||
|
|
||||||
|
A hierarchy of submodules cannot mix public and private modules. In other words,
|
||||||
|
if a module is public, its whole subtree must be public as well. For example,
|
||||||
|
having a public module `A` and private submodule `A.B` is forbidden and shall be
|
||||||
|
reported as an error during compilation. But having a private module `A` as well
|
||||||
|
as private module `A.B` is OK.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
_Types cannot be specified as private_, only constructors and methods. A type
|
||||||
|
must have all the constructors private or all the constructors public. This is
|
||||||
|
to prevent a situation when a pattern match can be done on public constructor,
|
||||||
|
but cannot be done on a private constructor from a different project. Mixing
|
||||||
|
public and private constructors in a single type is a compilation error. A type
|
||||||
|
with all constructors public is called an _open_ type and a type with all
|
||||||
|
constructors private is called a _closed_ type.
|
||||||
|
|
||||||
|
Methods on types (or on modules) can be specified private. To check whether a
|
||||||
|
private method is accessed only from within the same project, a runtime check
|
||||||
|
must be performed, as this cannot be checked during the compilation.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Lib/src/Pub_Type.enso:
|
||||||
|
|
||||||
|
```
|
||||||
|
type Pub_Type
|
||||||
|
Constructor field
|
||||||
|
private priv_method self = ...
|
||||||
|
pub_method self = self.field.to_text
|
||||||
|
|
||||||
|
private type Priv_Type
|
||||||
|
```
|
||||||
|
|
||||||
|
Lib/src/Methods.enso:
|
||||||
|
|
||||||
|
```
|
||||||
|
pub_stat_method x y = x + y
|
||||||
|
private priv_stat_method x y = x - y
|
||||||
|
```
|
||||||
|
|
||||||
|
Lib/src/Internal/Helpers.enso:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Mark the whole module as private
|
||||||
|
private
|
||||||
|
|
||||||
|
# OK to import private types in the same project
|
||||||
|
import project.Pub_Type.Priv_Type
|
||||||
|
```
|
||||||
|
|
||||||
|
Lib/src/Main.enso:
|
||||||
|
|
||||||
|
```
|
||||||
|
import project.Pub_Type.Pub_Type
|
||||||
|
export project.Pub_Type.Pub_Type
|
||||||
|
|
||||||
|
import project.Pub_Type.Priv_Type # OK - we can import private types in the same project.
|
||||||
|
export project.Pub_Type.Priv_Type # Failes at compile time - re-exporting private types is forbidden.
|
||||||
|
```
|
||||||
|
|
||||||
|
tmp.enso:
|
||||||
|
|
||||||
|
```
|
||||||
|
from Lib import Pub_Type
|
||||||
|
import Lib.Pub_Type.Priv_Type # Fails during compilation
|
||||||
|
import Lib.Methods
|
||||||
|
|
||||||
|
main =
|
||||||
|
# This constructor is not private, we can use it here.
|
||||||
|
obj = Pub_Type.Constructor field=42
|
||||||
|
obj.field # OK - Constructor is public, therefore, field is public
|
||||||
|
obj.priv_method # Runtime failure - priv_method is private
|
||||||
|
Pub_Type.priv_method self=obj # Runtime failure
|
||||||
|
obj.pub_method # OK
|
||||||
|
|
||||||
|
Lib.Pub_Type.Priv_Type # Fails at runtime - accessing private types via FQN is forbidden
|
||||||
|
|
||||||
|
Methods.pub_stat_method 1 2 # OK
|
||||||
|
Methods.priv_stat_method # Fails at runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
There shall be two checks. One check during **compilation**, that can be
|
||||||
|
implemented as a separate compiler pass, and that will ensure that no private
|
||||||
|
entity is _re-exported_ (exported from a module that is different from the
|
||||||
|
module inside which the entity is defined) and that for every type it holds that
|
||||||
|
either all the constructors are public or all the constructors are private
|
||||||
|
|
||||||
|
The second check shall be done during the **method/name resolution** step. This
|
||||||
|
step happens at runtime, before a method is called. After the method is
|
||||||
|
resolved, there shall be no further checks, so that the peak performance is not
|
||||||
|
affected.
|
||||||
|
|
||||||
|
## Performance impact
|
||||||
|
|
||||||
|
The performance hit on compilation time is minimal, as there are already dozens
|
||||||
|
of different compiler passes. Moreover, in the new compiler pass we shall check
|
||||||
|
only imports and exports statements, no other IR.
|
||||||
|
|
||||||
|
The performance hit on runtime, during method resolution, is minimal as well,
|
||||||
|
because it can be as easy as additional lookup in a hash map. Peak performance
|
||||||
|
will not be affected at all, as there are no further checks after method
|
||||||
|
resolution.
|
||||||
|
|
||||||
|
## Overcoming encapsulation
|
||||||
|
|
||||||
|
Sometimes it is useful to be able to access internal entities. Testing is the
|
||||||
|
most obvious example. Let's introduce a new CLI flag to the Engine launcher
|
||||||
|
called `--disable-private-check`, which will disable all the private checks
|
||||||
|
during compilation and method resolution.
|
||||||
|
|
||||||
|
## Other notes
|
||||||
|
|
||||||
|
- A private module implies that all the entities defined within are private
|
||||||
|
- A private type implies that all the constructors, methods and fields defined
|
||||||
|
within are private
|
||||||
|
- A private constructor implies private fields defined in that constructor.
|
@ -4,6 +4,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.enso.compiler.core.ir.Diagnostic;
|
||||||
import org.enso.compiler.core.ir.IdentifiedLocation;
|
import org.enso.compiler.core.ir.IdentifiedLocation;
|
||||||
import org.enso.compiler.core.ir.CallArgument;
|
import org.enso.compiler.core.ir.CallArgument;
|
||||||
import org.enso.compiler.core.ir.DefinitionArgument;
|
import org.enso.compiler.core.ir.DefinitionArgument;
|
||||||
@ -40,6 +41,7 @@ import org.enso.syntax2.Tree;
|
|||||||
|
|
||||||
import org.enso.syntax2.Tree.Invalid;
|
import org.enso.syntax2.Tree.Invalid;
|
||||||
|
|
||||||
|
import org.enso.syntax2.Tree.Private;
|
||||||
import scala.Option;
|
import scala.Option;
|
||||||
import scala.collection.immutable.LinearSeq;
|
import scala.collection.immutable.LinearSeq;
|
||||||
import scala.collection.immutable.List;
|
import scala.collection.immutable.List;
|
||||||
@ -152,9 +154,11 @@ final class TreeToIr {
|
|||||||
Module translateModule(Tree module) {
|
Module translateModule(Tree module) {
|
||||||
return switch (module) {
|
return switch (module) {
|
||||||
case Tree.BodyBlock b -> {
|
case Tree.BodyBlock b -> {
|
||||||
|
boolean isPrivate = false;
|
||||||
List<Definition> bindings = nil();
|
List<Definition> bindings = nil();
|
||||||
List<Import> imports = nil();
|
List<Import> imports = nil();
|
||||||
List<Export> exports = nil();
|
List<Export> exports = nil();
|
||||||
|
List<Diagnostic> diag = nil();
|
||||||
for (Line line : b.getStatements()) {
|
for (Line line : b.getStatements()) {
|
||||||
var expr = line.getExpression();
|
var expr = line.getExpression();
|
||||||
// Documentation found among imports/exports or at the top of the module (if it starts with imports) is
|
// Documentation found among imports/exports or at the top of the module (if it starts with imports) is
|
||||||
@ -169,6 +173,18 @@ final class TreeToIr {
|
|||||||
bindings = cons(c, bindings);
|
bindings = cons(c, bindings);
|
||||||
expr = doc.getExpression();
|
expr = doc.getExpression();
|
||||||
}
|
}
|
||||||
|
if (expr instanceof Private priv) {
|
||||||
|
if (priv.getBody() != null) {
|
||||||
|
var error = translateSyntaxError(priv, new Syntax.UnsupportedSyntax("Private token with body"));
|
||||||
|
diag = cons(error, diag);
|
||||||
|
}
|
||||||
|
if (isPrivate) {
|
||||||
|
var error = translateSyntaxError(priv, new Syntax.UnsupportedSyntax("Private token specified more than once"));
|
||||||
|
diag = cons(error, diag);
|
||||||
|
}
|
||||||
|
isPrivate = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
switch (expr) {
|
switch (expr) {
|
||||||
case Tree.Import imp -> imports = cons(translateImport(imp), imports);
|
case Tree.Import imp -> imports = cons(translateImport(imp), imports);
|
||||||
case Tree.Export exp -> exports = cons(translateExport(exp), exports);
|
case Tree.Export exp -> exports = cons(translateExport(exp), exports);
|
||||||
@ -176,11 +192,12 @@ final class TreeToIr {
|
|||||||
default -> bindings = translateModuleSymbol(expr, bindings);
|
default -> bindings = translateModuleSymbol(expr, bindings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
yield new Module(imports.reverse(), exports.reverse(), bindings.reverse(), getIdentifiedLocation(module), meta(), diag());
|
yield new Module(imports.reverse(), exports.reverse(), bindings.reverse(), isPrivate, getIdentifiedLocation(module), meta(), DiagnosticStorage.apply(diag));
|
||||||
}
|
}
|
||||||
default -> new Module(
|
default -> new Module(
|
||||||
nil(), nil(),
|
nil(), nil(),
|
||||||
cons(translateSyntaxError(module, new Syntax.UnsupportedSyntax("translateModule")), nil()),
|
cons(translateSyntaxError(module, new Syntax.UnsupportedSyntax("translateModule")), nil()),
|
||||||
|
false,
|
||||||
getIdentifiedLocation(module), meta(), diag()
|
getIdentifiedLocation(module), meta(), diag()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@ import org.enso.compiler.core.ir.module.scope.{Definition, Export, Import}
|
|||||||
* @param imports the import statements that bring other modules into scope
|
* @param imports the import statements that bring other modules into scope
|
||||||
* @param exports the export statements for this module
|
* @param exports the export statements for this module
|
||||||
* @param bindings the top-level bindings for this module
|
* @param bindings the top-level bindings for this module
|
||||||
|
* @param isPrivate whether or not this module is private (project-private)
|
||||||
* @param location the source location that the node corresponds to
|
* @param location the source location that the node corresponds to
|
||||||
* @param passData the pass metadata associated with this node
|
* @param passData the pass metadata associated with this node
|
||||||
* @param diagnostics compiler diagnostics for this node
|
* @param diagnostics compiler diagnostics for this node
|
||||||
@ -23,6 +24,7 @@ sealed case class Module(
|
|||||||
imports: List[Import],
|
imports: List[Import],
|
||||||
exports: List[Export],
|
exports: List[Export],
|
||||||
bindings: List[Definition],
|
bindings: List[Definition],
|
||||||
|
isPrivate: Boolean,
|
||||||
override val location: Option[IdentifiedLocation],
|
override val location: Option[IdentifiedLocation],
|
||||||
override val passData: MetadataStorage = MetadataStorage(),
|
override val passData: MetadataStorage = MetadataStorage(),
|
||||||
override val diagnostics: DiagnosticStorage = DiagnosticStorage()
|
override val diagnostics: DiagnosticStorage = DiagnosticStorage()
|
||||||
@ -51,7 +53,15 @@ sealed case class Module(
|
|||||||
id: Identifier = id
|
id: Identifier = id
|
||||||
): Module = {
|
): Module = {
|
||||||
val res =
|
val res =
|
||||||
Module(imports, exports, bindings, location, passData, diagnostics)
|
Module(
|
||||||
|
imports,
|
||||||
|
exports,
|
||||||
|
bindings,
|
||||||
|
isPrivate,
|
||||||
|
location,
|
||||||
|
passData,
|
||||||
|
diagnostics
|
||||||
|
)
|
||||||
res.id = id
|
res.id = id
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
@ -171,6 +171,38 @@ object ImportExport {
|
|||||||
s"No such constructor ${constructorName} in type $typeName"
|
s"No such constructor ${constructorName} in type $typeName"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case class ExportSymbolsFromPrivateModule(
|
||||||
|
moduleName: String
|
||||||
|
) extends Reason {
|
||||||
|
override def message: String =
|
||||||
|
s"Cannot export any symbol from module '$moduleName': The module is private"
|
||||||
|
}
|
||||||
|
|
||||||
|
case class ExportPrivateModule(
|
||||||
|
moduleName: String
|
||||||
|
) extends Reason {
|
||||||
|
override def message: String =
|
||||||
|
s"Cannot export private module '$moduleName'"
|
||||||
|
}
|
||||||
|
|
||||||
|
case class ImportPrivateModule(
|
||||||
|
moduleName: String
|
||||||
|
) extends Reason {
|
||||||
|
override def message: String =
|
||||||
|
s"Cannot import private module '$moduleName'"
|
||||||
|
}
|
||||||
|
|
||||||
|
case class SubmoduleVisibilityMismatch(
|
||||||
|
moduleName: String,
|
||||||
|
submoduleName: String,
|
||||||
|
moduleVisibility: String,
|
||||||
|
submoduleVisibility: String
|
||||||
|
) extends Reason {
|
||||||
|
override def message: String =
|
||||||
|
s"Cannot export submodule '$submoduleName' of module '$moduleName': " +
|
||||||
|
s"the submodule is $submoduleVisibility, but the module is $moduleVisibility"
|
||||||
|
}
|
||||||
|
|
||||||
/** Represents an ambiguous import resolution error, where the same symbol is imported more than once refereing
|
/** Represents an ambiguous import resolution error, where the same symbol is imported more than once refereing
|
||||||
* to different objects. The objects are represented by their physical path in the project.
|
* to different objects. The objects are represented by their physical path in the project.
|
||||||
*
|
*
|
||||||
|
@ -5,11 +5,14 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import org.enso.compiler.core.ir.Module;
|
import org.enso.compiler.core.ir.Module;
|
||||||
import org.junit.AfterClass;
|
import org.junit.AfterClass;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@ -1239,6 +1242,29 @@ public class EnsoParserTest {
|
|||||||
equivalenceTest("a = x", "a = SKIP FREEZE x.f y");
|
equivalenceTest("a = x", "a = SKIP FREEZE x.f y");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPrivateModules() throws Exception {
|
||||||
|
List<String> moduleCodes = List.of(
|
||||||
|
"private",
|
||||||
|
"""
|
||||||
|
# Comment
|
||||||
|
private
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
# Comment with empty line
|
||||||
|
|
||||||
|
private
|
||||||
|
"""
|
||||||
|
);
|
||||||
|
for (var moduleCode : moduleCodes) {
|
||||||
|
parseTest(moduleCode);
|
||||||
|
var module = compile("private");
|
||||||
|
assertTrue(module.isPrivate());
|
||||||
|
}
|
||||||
|
equivalenceTest("private", "# Line comment \nprivate");
|
||||||
|
equivalenceTest("private", "\n\nprivate");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void ise_184219679() throws IOException {
|
public void ise_184219679() throws IOException {
|
||||||
parseTest("""
|
parseTest("""
|
||||||
|
@ -0,0 +1,204 @@
|
|||||||
|
package org.enso.compiler.pass.analyse;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.enso.compiler.context.InlineContext;
|
||||||
|
import org.enso.compiler.context.ModuleContext;
|
||||||
|
import org.enso.compiler.core.IR;
|
||||||
|
import org.enso.compiler.core.ir.Expression;
|
||||||
|
import org.enso.compiler.core.ir.Module;
|
||||||
|
import org.enso.compiler.core.ir.expression.errors.ImportExport;
|
||||||
|
import org.enso.compiler.core.ir.module.scope.Export;
|
||||||
|
import org.enso.compiler.core.ir.module.scope.Import;
|
||||||
|
import org.enso.compiler.data.BindingsMap;
|
||||||
|
import org.enso.compiler.pass.IRPass;
|
||||||
|
import org.enso.interpreter.util.ScalaConversions;
|
||||||
|
import org.enso.pkg.QualifiedName;
|
||||||
|
import scala.Option;
|
||||||
|
import scala.collection.immutable.Seq;
|
||||||
|
import scala.jdk.javaapi.CollectionConverters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates through all the imports and exports of non-synthetic modules and ensures that:
|
||||||
|
* <ul>
|
||||||
|
* <li>No private module is exported</li>
|
||||||
|
* <li>No private module from a different project is imported</li>
|
||||||
|
* <li>Hierarchy of modules and submodules does not mix private and public modules</li>
|
||||||
|
* </ul>
|
||||||
|
* Inserts errors into imports/exports IRs if the above conditions are violated.
|
||||||
|
*/
|
||||||
|
public final class PrivateModuleAnalysis implements IRPass {
|
||||||
|
private static final PrivateModuleAnalysis singleton = new PrivateModuleAnalysis();
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
private PrivateModuleAnalysis() {}
|
||||||
|
|
||||||
|
public static PrivateModuleAnalysis getInstance() {
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void org$enso$compiler$pass$IRPass$_setter_$key_$eq(UUID v) {
|
||||||
|
this.uuid = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID key() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Seq<IRPass> precursorPasses() {
|
||||||
|
List<IRPass> passes = List.of(
|
||||||
|
BindingAnalysis$.MODULE$,
|
||||||
|
ImportSymbolAnalysis$.MODULE$
|
||||||
|
);
|
||||||
|
return CollectionConverters.asScala(passes).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Seq<IRPass> invalidatedPasses() {
|
||||||
|
return ScalaConversions.nil();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Module runModule(Module moduleIr, ModuleContext moduleContext) {
|
||||||
|
var bindingsMap = (BindingsMap) moduleIr.passData().get(BindingAnalysis$.MODULE$).get();
|
||||||
|
var currentPackage = moduleContext.getPackage();
|
||||||
|
List<Import> importErrors = new ArrayList<>();
|
||||||
|
List<Export> exportErrors = new ArrayList<>();
|
||||||
|
var isCurrentModulePrivate = moduleIr.isPrivate();
|
||||||
|
|
||||||
|
// Ensure that imported modules from a different project are not private.
|
||||||
|
bindingsMap.resolvedImports().foreach(resolvedImp -> {
|
||||||
|
var importedModule = resolvedImp.target().module().unsafeAsModule("should succeed");
|
||||||
|
var importedModuleName = importedModule.getName().toString();
|
||||||
|
var importedModulePackage = importedModule.getPackage();
|
||||||
|
if (currentPackage != null
|
||||||
|
&& !currentPackage.equals(importedModulePackage)
|
||||||
|
&& importedModule.isPrivate()) {
|
||||||
|
importErrors.add(ImportExport.apply(
|
||||||
|
resolvedImp.importDef(),
|
||||||
|
new ImportExport.ImportPrivateModule(importedModuleName),
|
||||||
|
ImportExport.apply$default$3(),
|
||||||
|
ImportExport.apply$default$4()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure that no symbols are exported from a private module.
|
||||||
|
if (isCurrentModulePrivate && containsExport(moduleIr)) {
|
||||||
|
exportErrors.add(ImportExport.apply(
|
||||||
|
moduleIr.exports().apply(0),
|
||||||
|
new ImportExport.ExportSymbolsFromPrivateModule(moduleContext.getName().toString()),
|
||||||
|
ImportExport.apply$default$3(),
|
||||||
|
ImportExport.apply$default$4()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Ensure that private modules are not exported and that the hierarchy of submodules
|
||||||
|
// does not mix public and private modules.
|
||||||
|
bindingsMap
|
||||||
|
.getDirectlyExportedModules()
|
||||||
|
.foreach(expModule -> {
|
||||||
|
var expModuleRef = expModule.target().module().unsafeAsModule("should succeed");
|
||||||
|
if (expModuleRef.isPrivate()) {
|
||||||
|
var associatedExportIR = findExportIRByName(moduleIr, expModuleRef.getName());
|
||||||
|
assert associatedExportIR.isDefined();
|
||||||
|
if (isSubmoduleName(moduleContext.getName(), expModuleRef.getName())) {
|
||||||
|
var haveSameVisibility = isCurrentModulePrivate == expModuleRef.isPrivate();
|
||||||
|
if (!haveSameVisibility) {
|
||||||
|
exportErrors.add(
|
||||||
|
ImportExport.apply(
|
||||||
|
associatedExportIR.get(),
|
||||||
|
new ImportExport.SubmoduleVisibilityMismatch(
|
||||||
|
moduleContext.getName().toString(),
|
||||||
|
expModuleRef.getName().toString(),
|
||||||
|
isCurrentModulePrivate ? "private" : "public",
|
||||||
|
expModuleRef.isPrivate() ? "private" : "public"
|
||||||
|
),
|
||||||
|
ImportExport.apply$default$3(),
|
||||||
|
ImportExport.apply$default$4()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exportErrors.add(
|
||||||
|
ImportExport.apply(
|
||||||
|
associatedExportIR.get(),
|
||||||
|
new ImportExport.ExportPrivateModule(expModuleRef.getName().toString()),
|
||||||
|
ImportExport.apply$default$3(),
|
||||||
|
ImportExport.apply$default$4()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
scala.collection.immutable.List<Import> convertedImports =
|
||||||
|
importErrors.isEmpty() ? moduleIr.imports() : CollectionConverters.asScala(importErrors).toList();
|
||||||
|
scala.collection.immutable.List<Export> convertedExports =
|
||||||
|
exportErrors.isEmpty() ? moduleIr.exports() : CollectionConverters.asScala(exportErrors).toList();
|
||||||
|
|
||||||
|
return moduleIr.copy(
|
||||||
|
convertedImports,
|
||||||
|
convertedExports,
|
||||||
|
moduleIr.bindings(),
|
||||||
|
moduleIr.location(),
|
||||||
|
moduleIr.passData(),
|
||||||
|
moduleIr.diagnostics(),
|
||||||
|
moduleIr.id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSubmoduleName(QualifiedName parentModName, QualifiedName subModName) {
|
||||||
|
if (subModName.getParent().isDefined()) {
|
||||||
|
return parentModName.item().equals(
|
||||||
|
subModName.getParent().get().item()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Expression runExpression(Expression ir, InlineContext inlineContext) {
|
||||||
|
return ir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true iff the given Module's IR contains an export that is not synthetic.
|
||||||
|
*/
|
||||||
|
private static boolean containsExport(Module moduleIr) {
|
||||||
|
return !moduleIr.exports().isEmpty() && moduleIr.exports().exists(exp -> {
|
||||||
|
if (exp instanceof Export.Module moduleExport) {
|
||||||
|
return !moduleExport.isSynthetic();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Option<Export> findExportIRByName(Module moduleIr, QualifiedName fqn) {
|
||||||
|
return moduleIr.exports().find(exp -> {
|
||||||
|
if (exp instanceof Export.Module expMod) {
|
||||||
|
if (expMod.name().parts().last().name().equals(fqn.item())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("unknown exp: " + exp);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T extends IR> T updateMetadataInDuplicate(T sourceIr, T copyOfIr) {
|
||||||
|
return IRPass.super.updateMetadataInDuplicate(sourceIr, copyOfIr);
|
||||||
|
}
|
||||||
|
}
|
@ -193,6 +193,11 @@ public final class Module implements EnsoObject {
|
|||||||
return synthetic;
|
return synthetic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return true iff this module is private (project-private). */
|
||||||
|
public boolean isPrivate() {
|
||||||
|
return ir.isPrivate();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets new literal sources for the module.
|
* Sets new literal sources for the module.
|
||||||
*
|
*
|
||||||
|
@ -28,4 +28,14 @@ public class ScalaConversions {
|
|||||||
public static <T> List<T> asJava(Seq<T> list) {
|
public static <T> List<T> asJava(Seq<T> list) {
|
||||||
return CollectionConverters.asJava(list);
|
return CollectionConverters.asJava(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> scala.collection.immutable.List<T> nil() {
|
||||||
|
return (scala.collection.immutable.List<T>) scala.collection.immutable.Nil$.MODULE$;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> scala.collection.immutable.List<T> cons(
|
||||||
|
T head, scala.collection.immutable.List<T> tail) {
|
||||||
|
return scala.collection.immutable.$colon$colon$.MODULE$.apply(head, tail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ class Passes(
|
|||||||
LambdaShorthandToLambda,
|
LambdaShorthandToLambda,
|
||||||
ImportSymbolAnalysis,
|
ImportSymbolAnalysis,
|
||||||
AmbiguousImportsAnalysis,
|
AmbiguousImportsAnalysis,
|
||||||
|
PrivateModuleAnalysis.getInstance(),
|
||||||
ShadowedPatternFields,
|
ShadowedPatternFields,
|
||||||
UnreachableMatchBranches,
|
UnreachableMatchBranches,
|
||||||
NestedPatternMatch,
|
NestedPatternMatch,
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
name: Test_Private_Modules_1
|
||||||
|
license: APLv2
|
||||||
|
enso-version: default
|
||||||
|
version: "0.0.1"
|
||||||
|
author: "Enso Team <contact@enso.org>"
|
||||||
|
maintainer: "Enso Team <contact@enso.org>"
|
||||||
|
prefer-local-libraries: true
|
@ -0,0 +1,9 @@
|
|||||||
|
# Export some "public" stuff from this library
|
||||||
|
from project.Pub_Mod import Pub_Mod_Type
|
||||||
|
from project.Pub_Mod export Pub_Mod_Type
|
||||||
|
|
||||||
|
# Can import and use private modules in the same project
|
||||||
|
from project.Priv_Mod import Type_In_Priv_Mod
|
||||||
|
|
||||||
|
main =
|
||||||
|
Type_In_Priv_Mod.Value 42 . data
|
@ -0,0 +1,4 @@
|
|||||||
|
private
|
||||||
|
|
||||||
|
type Type_In_Priv_Mod
|
||||||
|
Value data
|
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
type Pub_Mod_Type
|
||||||
|
Value data
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
name: Test_Private_Modules_2
|
||||||
|
license: APLv2
|
||||||
|
enso-version: default
|
||||||
|
version: "0.0.1"
|
||||||
|
author: "Enso Team <contact@enso.org>"
|
||||||
|
maintainer: "Enso Team <contact@enso.org>"
|
||||||
|
prefer-local-libraries: true
|
@ -0,0 +1,5 @@
|
|||||||
|
# Will import just exported (and public) symbols from Test_Private_Modules_1
|
||||||
|
from local.Test_Private_Modules_1 import all
|
||||||
|
|
||||||
|
main =
|
||||||
|
Pub_Mod_Type.Value 42
|
@ -0,0 +1,7 @@
|
|||||||
|
name: Test_Private_Modules_3
|
||||||
|
license: APLv2
|
||||||
|
enso-version: default
|
||||||
|
version: "0.0.1"
|
||||||
|
author: "Enso Team <contact@enso.org>"
|
||||||
|
maintainer: "Enso Team <contact@enso.org>"
|
||||||
|
prefer-local-libraries: true
|
@ -0,0 +1,5 @@
|
|||||||
|
# This import should fail - importing private module
|
||||||
|
import local.Test_Private_Modules_1.Priv_Mod
|
||||||
|
|
||||||
|
main =
|
||||||
|
"Success"
|
@ -0,0 +1,7 @@
|
|||||||
|
name: Test_Private_Modules_4
|
||||||
|
license: APLv2
|
||||||
|
enso-version: default
|
||||||
|
version: "0.0.1"
|
||||||
|
author: "Enso Team <contact@enso.org>"
|
||||||
|
maintainer: "Enso Team <contact@enso.org>"
|
||||||
|
prefer-local-libraries: true
|
@ -0,0 +1,7 @@
|
|||||||
|
import project.Sub
|
||||||
|
export project.Sub
|
||||||
|
|
||||||
|
# Fails at compile time - cannot mix private and public modules in a module subtree.
|
||||||
|
|
||||||
|
main =
|
||||||
|
42
|
@ -0,0 +1,4 @@
|
|||||||
|
private
|
||||||
|
|
||||||
|
foo = 23
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
foo = 42
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
name: Test_Private_Modules_5
|
||||||
|
license: APLv2
|
||||||
|
enso-version: default
|
||||||
|
version: "0.0.1"
|
||||||
|
author: "Enso Team <contact@enso.org>"
|
||||||
|
maintainer: "Enso Team <contact@enso.org>"
|
||||||
|
prefer-local-libraries: true
|
@ -0,0 +1,7 @@
|
|||||||
|
# This project has a private module tree
|
||||||
|
|
||||||
|
import project.Sub.Priv
|
||||||
|
|
||||||
|
main =
|
||||||
|
Priv.foo
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
# This is a private non-synthetic module with submodules
|
||||||
|
private
|
||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
private
|
||||||
|
|
||||||
|
foo = 42
|
@ -258,7 +258,7 @@ trait CompilerRunner {
|
|||||||
runtime.Module.empty(QualifiedName.simpleName("Test_Module"), null)
|
runtime.Module.empty(QualifiedName.simpleName("Test_Module"), null)
|
||||||
ModuleTestUtils.unsafeSetIr(
|
ModuleTestUtils.unsafeSetIr(
|
||||||
mod,
|
mod,
|
||||||
Module(List(), List(), List(), None)
|
Module(List(), List(), List(), false, None)
|
||||||
.updateMetadata(
|
.updateMetadata(
|
||||||
BindingAnalysis -->> BindingsMap(
|
BindingAnalysis -->> BindingsMap(
|
||||||
List(),
|
List(),
|
||||||
|
@ -9,7 +9,8 @@ import org.enso.compiler.pass.analyse.{
|
|||||||
AliasAnalysis,
|
AliasAnalysis,
|
||||||
AmbiguousImportsAnalysis,
|
AmbiguousImportsAnalysis,
|
||||||
BindingAnalysis,
|
BindingAnalysis,
|
||||||
ImportSymbolAnalysis
|
ImportSymbolAnalysis,
|
||||||
|
PrivateModuleAnalysis
|
||||||
}
|
}
|
||||||
import org.enso.compiler.pass.desugar._
|
import org.enso.compiler.pass.desugar._
|
||||||
import org.enso.compiler.pass.lint.{ModuleNameConflicts, ShadowedPatternFields}
|
import org.enso.compiler.pass.lint.{ModuleNameConflicts, ShadowedPatternFields}
|
||||||
@ -60,6 +61,7 @@ class PassesTest extends CompilerTest {
|
|||||||
LambdaShorthandToLambda,
|
LambdaShorthandToLambda,
|
||||||
ImportSymbolAnalysis,
|
ImportSymbolAnalysis,
|
||||||
AmbiguousImportsAnalysis,
|
AmbiguousImportsAnalysis,
|
||||||
|
PrivateModuleAnalysis.getInstance(),
|
||||||
ShadowedPatternFields,
|
ShadowedPatternFields,
|
||||||
UnreachableMatchBranches,
|
UnreachableMatchBranches,
|
||||||
NestedPatternMatch,
|
NestedPatternMatch,
|
||||||
|
@ -98,6 +98,7 @@ class GatherDiagnosticsTest extends CompilerTest {
|
|||||||
definition.Method
|
definition.Method
|
||||||
.Explicit(method2Ref, error3, None)
|
.Explicit(method2Ref, error3, None)
|
||||||
),
|
),
|
||||||
|
false,
|
||||||
None
|
None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -905,4 +905,97 @@ class ImportExportTest
|
|||||||
mainIr.imports.head.isInstanceOf[errors.ImportExport] shouldBe false
|
mainIr.imports.head.isInstanceOf[errors.ImportExport] shouldBe false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"Private modules" should {
|
||||||
|
"not be able to export private module" in {
|
||||||
|
"""
|
||||||
|
|private
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(
|
||||||
|
packageQualifiedName.createChild("Priv_Module")
|
||||||
|
)
|
||||||
|
|
||||||
|
val mainIr = s"""
|
||||||
|
|import $namespace.$packageName.Priv_Module
|
||||||
|
|export $namespace.$packageName.Priv_Module
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(packageQualifiedName.createChild("Main"))
|
||||||
|
.getIr
|
||||||
|
|
||||||
|
mainIr.exports.size shouldEqual 1
|
||||||
|
mainIr.exports.head.isInstanceOf[errors.ImportExport] shouldBe true
|
||||||
|
mainIr.exports.head
|
||||||
|
.asInstanceOf[errors.ImportExport]
|
||||||
|
.reason
|
||||||
|
.isInstanceOf[errors.ImportExport.ExportPrivateModule] shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
"not be able to export anything from private module itself" in {
|
||||||
|
val mainIr =
|
||||||
|
s"""
|
||||||
|
|private
|
||||||
|
|
|
||||||
|
|from $namespace.$packageName export Type_In_Priv_Module
|
||||||
|
|
|
||||||
|
|type Type_In_Priv_Module
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(packageQualifiedName.createChild("Main"))
|
||||||
|
.getIr
|
||||||
|
|
||||||
|
mainIr.exports.size shouldEqual 1
|
||||||
|
mainIr.exports.head.isInstanceOf[errors.ImportExport] shouldBe true
|
||||||
|
mainIr.exports.head
|
||||||
|
.asInstanceOf[errors.ImportExport]
|
||||||
|
.reason
|
||||||
|
.isInstanceOf[
|
||||||
|
errors.ImportExport.ExportSymbolsFromPrivateModule
|
||||||
|
] shouldBe true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
"not be able to export anything from private module from Main module" in {
|
||||||
|
"""
|
||||||
|
|private
|
||||||
|
|type Type_In_Priv_Module
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(
|
||||||
|
packageQualifiedName.createChild("Priv_Module")
|
||||||
|
)
|
||||||
|
|
||||||
|
val mainIr =
|
||||||
|
s"""
|
||||||
|
|from $namespace.$packageName.Priv_Module import Type_In_Priv_Module
|
||||||
|
|from $namespace.$packageName.Priv_Module export Type_In_Priv_Module
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(packageQualifiedName.createChild("Main"))
|
||||||
|
.getIr
|
||||||
|
|
||||||
|
mainIr.exports.size shouldEqual 1
|
||||||
|
mainIr.exports.head.isInstanceOf[errors.ImportExport] shouldBe true
|
||||||
|
mainIr.exports.head
|
||||||
|
.asInstanceOf[errors.ImportExport]
|
||||||
|
.reason
|
||||||
|
.isInstanceOf[errors.ImportExport.ExportPrivateModule] shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
"be able to import stuff from private modules in the same library" in {
|
||||||
|
"""
|
||||||
|
|private
|
||||||
|
|type Type_In_Priv_Module
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(
|
||||||
|
packageQualifiedName.createChild("Priv_Module")
|
||||||
|
)
|
||||||
|
|
||||||
|
val mainIr =
|
||||||
|
s"""
|
||||||
|
|from $namespace.$packageName.Priv_Module import Type_In_Priv_Module
|
||||||
|
|""".stripMargin
|
||||||
|
.createModule(packageQualifiedName.createChild("Main"))
|
||||||
|
.getIr
|
||||||
|
|
||||||
|
val errors = mainIr.preorder.filter(x => x.isInstanceOf[Error])
|
||||||
|
errors.size shouldEqual 0
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,4 +219,41 @@ class ImportsTest extends PackageTest {
|
|||||||
outLines(2) shouldEqual "(D_Mod.Value 1)"
|
outLines(2) shouldEqual "(D_Mod.Value 1)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"Private modules" should "be able to import and use private modules within the same project" in {
|
||||||
|
evalTestProject(
|
||||||
|
"Test_Private_Modules_1"
|
||||||
|
).toString shouldEqual "42"
|
||||||
|
}
|
||||||
|
|
||||||
|
"Private modules" should "be able to import non-private stuff" in {
|
||||||
|
evalTestProject(
|
||||||
|
"Test_Private_Modules_2"
|
||||||
|
).toString shouldEqual "(Pub_Mod_Type.Value 42)"
|
||||||
|
}
|
||||||
|
|
||||||
|
"Private modules" should "not be able to import private modules from different project" in {
|
||||||
|
the[InterpreterException] thrownBy evalTestProject(
|
||||||
|
"Test_Private_Modules_3"
|
||||||
|
) should have message "Compilation aborted due to errors."
|
||||||
|
val outLines = consumeOut.filterNot(isDiagnosticLine)
|
||||||
|
outLines should have length 1
|
||||||
|
outLines.head should include(
|
||||||
|
"Main.enso:2:1: error: Cannot import private module"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"Private modules" should "not be able to mix private and public submodules" in {
|
||||||
|
val e = the[InterpreterException] thrownBy evalTestProject(
|
||||||
|
"Test_Private_Modules_4"
|
||||||
|
)
|
||||||
|
e.getMessage() should include(
|
||||||
|
"Cannot export submodule 'local.Test_Private_Modules_4.Sub.Priv_SubMod' of module 'local.Test_Private_Modules_4.Sub'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
"Private module" should "be able to have only private submodules" in {
|
||||||
|
evalTestProject(
|
||||||
|
"Test_Private_Modules_5"
|
||||||
|
) shouldEqual 42
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,7 @@ where T: serde::Serialize + Reflect {
|
|||||||
TextEnd::reflect(),
|
TextEnd::reflect(),
|
||||||
TextStart::reflect(),
|
TextStart::reflect(),
|
||||||
Wildcard::reflect(),
|
Wildcard::reflect(),
|
||||||
|
Private::reflect(),
|
||||||
];
|
];
|
||||||
skip_tokens.into_iter().for_each(|token| to_s_expr.skip(rust_to_meta[&token.id]));
|
skip_tokens.into_iter().for_each(|token| to_s_expr.skip(rust_to_meta[&token.id]));
|
||||||
let ident_token = rust_to_meta[&Ident::reflect().id];
|
let ident_token = rust_to_meta[&Ident::reflect().id];
|
||||||
|
@ -1189,6 +1189,35 @@ fn pattern_match_auto_scope() {
|
|||||||
test(&code.join("\n"), expected);
|
test(&code.join("\n"), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Private (project-private) keyword ===
|
||||||
|
#[test]
|
||||||
|
fn private_keyword() {
|
||||||
|
test("private", block![(Private())]);
|
||||||
|
test("private func", block![(Private (Ident func))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn private_is_first_statement() {
|
||||||
|
// Comments and empty lines are allowed before `private`.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let lines = vec![
|
||||||
|
"# Some comment",
|
||||||
|
"# Other comment",
|
||||||
|
"",
|
||||||
|
"private"
|
||||||
|
];
|
||||||
|
test(&lines.join("\n"), block![()()()(Private)]);
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let lines = vec![
|
||||||
|
"type T",
|
||||||
|
"",
|
||||||
|
"private"
|
||||||
|
];
|
||||||
|
expect_invalid_node(&lines.join("\n"));
|
||||||
|
}
|
||||||
|
|
||||||
// === Array/tuple literals ===
|
// === Array/tuple literals ===
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ fn statement() -> resolver::SegmentMap<'static> {
|
|||||||
register_import_macros(&mut macro_map);
|
register_import_macros(&mut macro_map);
|
||||||
register_export_macros(&mut macro_map);
|
register_export_macros(&mut macro_map);
|
||||||
macro_map.register(type_def());
|
macro_map.register(type_def());
|
||||||
|
macro_map.register(private());
|
||||||
macro_map.register(foreign());
|
macro_map.register(foreign());
|
||||||
macro_map
|
macro_map
|
||||||
}
|
}
|
||||||
@ -672,6 +673,10 @@ fn foreign<'s>() -> Definition<'s> {
|
|||||||
crate::macro_definition! {("foreign", everything()) foreign_body}
|
crate::macro_definition! {("foreign", everything()) foreign_body}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn private<'s>() -> Definition<'s> {
|
||||||
|
crate::macro_definition! {("private", everything()) private_keyword}
|
||||||
|
}
|
||||||
|
|
||||||
fn skip<'s>() -> Definition<'s> {
|
fn skip<'s>() -> Definition<'s> {
|
||||||
crate::macro_definition! {("SKIP", everything()) capture_expressions}
|
crate::macro_definition! {("SKIP", everything()) capture_expressions}
|
||||||
}
|
}
|
||||||
@ -680,6 +685,16 @@ fn freeze<'s>() -> Definition<'s> {
|
|||||||
crate::macro_definition! {("FREEZE", everything()) capture_expressions}
|
crate::macro_definition! {("FREEZE", everything()) capture_expressions}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn private_keyword<'s>(
|
||||||
|
segments: NonEmptyVec<MatchedSegment<'s>>,
|
||||||
|
precedence: &mut operator::Precedence<'s>,
|
||||||
|
) -> syntax::Tree<'s> {
|
||||||
|
let segment = segments.pop().0;
|
||||||
|
let keyword = into_private(segment.header);
|
||||||
|
let body = precedence.resolve(segment.result.tokens());
|
||||||
|
syntax::Tree::private(keyword, body)
|
||||||
|
}
|
||||||
|
|
||||||
/// Macro body builder that just parses the tokens of each segment as expressions, and places them
|
/// Macro body builder that just parses the tokens of each segment as expressions, and places them
|
||||||
/// in a [`MultiSegmentApp`].
|
/// in a [`MultiSegmentApp`].
|
||||||
fn capture_expressions<'s>(
|
fn capture_expressions<'s>(
|
||||||
@ -783,6 +798,11 @@ fn into_ident(token: syntax::token::Token) -> syntax::token::Ident {
|
|||||||
syntax::token::ident(left_offset, code, false, 0, false, false, false)
|
syntax::token::ident(left_offset, code, false, 0, false, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_private(token: syntax::token::Token) -> syntax::token::Private {
|
||||||
|
let syntax::token::Token { left_offset, code, .. } = token;
|
||||||
|
syntax::token::private(left_offset, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// === Validators ===
|
// === Validators ===
|
||||||
|
|
||||||
|
@ -281,6 +281,7 @@ macro_rules! with_token_definition { ($f:ident ($($args:tt)*)) => { $f! { $($arg
|
|||||||
pub base: Option<Base>
|
pub base: Option<Base>
|
||||||
},
|
},
|
||||||
NumberBase,
|
NumberBase,
|
||||||
|
Private,
|
||||||
TextStart,
|
TextStart,
|
||||||
TextEnd,
|
TextEnd,
|
||||||
TextSection,
|
TextSection,
|
||||||
|
@ -112,6 +112,11 @@ macro_rules! with_ast_definition { ($f:ident ($($args:tt)*)) => { $f! { $($args)
|
|||||||
Ident {
|
Ident {
|
||||||
pub token: token::Ident<'s>,
|
pub token: token::Ident<'s>,
|
||||||
},
|
},
|
||||||
|
/// A `private` keyword, marking associated expressions as project-private.
|
||||||
|
Private {
|
||||||
|
pub keyword: token::Private<'s>,
|
||||||
|
pub body: Option<Tree<'s>>,
|
||||||
|
},
|
||||||
/// A numeric literal, like `10`.
|
/// A numeric literal, like `10`.
|
||||||
Number {
|
Number {
|
||||||
pub base: Option<token::NumberBase<'s>>,
|
pub base: Option<token::NumberBase<'s>>,
|
||||||
@ -990,6 +995,7 @@ pub fn to_ast(token: Token) -> Tree {
|
|||||||
| token::Variant::BlockEnd(_)
|
| token::Variant::BlockEnd(_)
|
||||||
// This should be unreachable: `Precedence::resolve` doesn't calls `to_ast` for operators.
|
// This should be unreachable: `Precedence::resolve` doesn't calls `to_ast` for operators.
|
||||||
| token::Variant::Operator(_)
|
| token::Variant::Operator(_)
|
||||||
|
| token::Variant::Private(_)
|
||||||
// Map an error case in the lexer to an error in the AST.
|
// Map an error case in the lexer to an error in the AST.
|
||||||
| token::Variant::Invalid(_) => {
|
| token::Variant::Invalid(_) => {
|
||||||
let message = format!("Unexpected token: {token:?}");
|
let message = format!("Unexpected token: {token:?}");
|
||||||
|
Loading…
Reference in New Issue
Block a user