Skip to content

Check private access at resolution, not an invocation #13392

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

Merged
merged 35 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1a8f5bc
Revert 08b04dd2300c3d3b2a8fc49d16742c7c85d6f7ee
Akirathan Jun 27, 2025
142fecf
Revert d9cc084c2348e28856d748d15f0bef94c146dc39
Akirathan Jun 27, 2025
e721cf5
Add minimal reproducer
Akirathan Jun 30, 2025
9dc390b
test calling private method directly or via lambda
Akirathan Jul 1, 2025
78f2b81
Copy projectPrivate property from FunctionSchema when mapping call ar…
Akirathan Jul 1, 2025
6c66ef3
Revert "Revert d9cc084c2348e28856d748d15f0bef94c146dc39"
Akirathan Jul 1, 2025
d23375b
Revert "Revert 08b04dd2300c3d3b2a8fc49d16742c7c85d6f7ee"
Akirathan Jul 1, 2025
f28e368
Private constructor cannot be called via callback.
Akirathan Jul 1, 2025
8782c0b
fmt
Akirathan Jul 1, 2025
3786693
Fix the expected evaluation semantics in tests.
Akirathan Jul 2, 2025
b72747b
Update private methods semantics in tests
Akirathan Jul 3, 2025
c808400
Shouldn't be possible to access private field
Akirathan Jul 3, 2025
daa591b
Remove leftover comment
Akirathan Jul 3, 2025
2098788
Fix encapsulation.md docs
Akirathan Jul 3, 2025
289684f
Revert "Copy projectPrivate property from FunctionSchema when mapping…
Akirathan Jul 14, 2025
3685ee6
Better test name
Akirathan Jul 14, 2025
8f1f229
Better test name
Akirathan Jul 14, 2025
ebdb0fd
Update docs
Akirathan Jul 14, 2025
3e67857
Add test for calling private static method that was directly imported
Akirathan Jul 14, 2025
d1d4975
No private checking in InvokeFunctionNode
Akirathan Jul 14, 2025
b636594
Update tests: Private field cannot be accessed
Akirathan Jul 14, 2025
1e884d2
Private check is inside UnresolvedSymbol
Akirathan Jul 14, 2025
961a412
fmt
Akirathan Jul 14, 2025
4f58cbc
Fix expected cons name in test
Akirathan Jul 14, 2025
2acd478
Update tests: Private field cannot be accessed
Akirathan Jul 14, 2025
e84da7c
Revert 08b04dd2300c3d3b2a8fc49d16742c7c85d6f7ee
Akirathan Jun 27, 2025
ed3b014
Revert d9cc084c2348e28856d748d15f0bef94c146dc39
Akirathan Jun 27, 2025
a20a46c
Merge branch 'develop' into wip/akirathan/13078-private-generated-get…
Akirathan Jul 16, 2025
ce79c7f
Add CHANGELOG entry
Akirathan Jul 16, 2025
3c98205
Do not check private access if private check is disabled
Akirathan Jul 16, 2025
72ccc50
Private check in UnresolvedSymbol compares project from correct modul…
Akirathan Jul 21, 2025
20cdc32
Add test for private check of UnresolvedSymbol
Akirathan Jul 21, 2025
286bfec
Merge branch 'develop' into wip/akirathan/13078-private-generated-get…
Akirathan Jul 21, 2025
e407dba
Fix tests in Meta_Spec.
Akirathan Jul 21, 2025
89a722e
Fix test in Private_Spec - it is possible to call private constructor…
Akirathan Jul 21, 2025
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,15 @@
[Python](https://www.graalvm.org/python/)) to version `24.2.0`.
- [Upgrade GraalVM from JDK 21 to JDK 24][12855]
- [Use JAVA_TOOL_OPTIONS env variable to alter JVM arguments][13256]
- [Check private access at resolution, not on invocation][13392]

[12500]: https://github.com/enso-org/enso/pull/12500
[12976]: https://github.com/enso-org/enso/pull/12976
[12855]: https://github.com/enso-org/enso/pull/12855
[12905]: https://github.com/enso-org/enso/pull/12905
[13225]: https://github.com/enso-org/enso/pull/13225
[13256]: https://github.com/enso-org/enso/pull/13256
[13392]: https://github.com/enso-org/enso/pull/13392

# Enso 2025.1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,21 @@ type Decomposed_S3_Path
key self -> Text =
add_directory_suffix = self.parts.not_empty && self.parts.last.is_directory
suffix = if add_directory_suffix then S3_Path.delimiter else ""
self.parts.map (\x -> x.name) . join separator=S3_Path.delimiter suffix=suffix
self.parts.map .name . join separator=S3_Path.delimiter suffix=suffix

parse (key : Text) -> Decomposed_S3_Path =
has_directory_suffix = key.ends_with S3_Path.delimiter
parts = key.split S3_Path.delimiter . filter (p-> p.is_empty.not)
entries = case has_directory_suffix of
True -> parts.map \x -> Path_Entry.Directory x
True -> parts.map Path_Entry.Directory
False ->
if parts.is_empty then [] else
(parts.drop (..Last 1) . map \x -> Path_Entry.Directory x) + [Path_Entry.File parts.last]
(parts.drop (..Last 1) . map Path_Entry.Directory) + [Path_Entry.File parts.last]
Decomposed_S3_Path.Value entries

join (paths : Vector Decomposed_S3_Path) -> Decomposed_S3_Path =
if paths.is_empty then Error.throw (Illegal_Argument.Error "Cannot join an empty list of paths.") else
flattened = paths.flat_map \x -> x.parts
flattened = paths.flat_map .parts
# Any `File` parts from the middle are now transformed to `Directory`:
aligned = flattened.map_with_index ix-> part-> case part of
Path_Entry.Directory _ -> part
Expand All @@ -46,7 +46,7 @@ type Decomposed_S3_Path
Decomposed_S3_Path.Value aligned

normalize self -> Decomposed_S3_Path =
new_parts = Path_Helpers.normalize_segments self.parts \x -> x.name
new_parts = Path_Helpers.normalize_segments self.parts .name
Decomposed_S3_Path.Value new_parts

parent self -> Decomposed_S3_Path | Nothing =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ aggregate table:DB_Table group_by:(Vector | Text | Integer | Regex) columns:Vect

new_columns = partitioned.second
problem_builder.attach_problems_before on_problems <|
problems = partitioned.first.map \x->x.value
problems = partitioned.first.map .value
on_problems.attach_problems_before problems <|
handle_no_output_columns =
first_problem = if problems.is_empty then Nothing else problems.first
Expand Down
3 changes: 2 additions & 1 deletion docs/semantics/encapsulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ main =
obj.pub_method # OK

# This constructor is private, we have to use factory method.
opaque = Closed_Type.Constructor field=42
# Note that directly calling `Closed_Type.Constructor` would fail.
opaque = Closed_Type.factory field=42
opaque.field # Runtime failure - Constructor is private, therefore, no getter is generated
opaque.priv_method # Runtime failure - priv_method is private
Closed_Type.priv_method self=opaque # Runtime failure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@

import com.oracle.truffle.api.interop.InteropLibrary;
import java.io.IOException;
import java.util.Set;
import org.enso.common.RuntimeOptions;
import org.enso.pkg.QualifiedName;
import org.enso.polyglot.PolyglotContext;
import org.enso.test.utils.ContextUtils;
import org.enso.test.utils.ProjectUtils;
import org.enso.test.utils.SourceModule;
import org.graalvm.polyglot.PolyglotException;
import org.junit.Rule;
import org.junit.Test;
Expand Down Expand Up @@ -155,4 +158,94 @@ public void cannotPatternMatchOnPrivateConstructorFromDifferentProject() throws
}
}
}

@Test
public void canCallPrivateConstructor_ViaCallback() throws Exception {
var libDir = tempFolder.newFolder("Lib").toPath();
ProjectUtils.createProject(
"Lib",
Set.of(
new SourceModule(
QualifiedName.fromString("My_Type"),
"""
private

type My_Type
Cons name
"""),
new SourceModule(
QualifiedName.fromString("Main"),
"""
import project.My_Type.My_Type

call_method ~callback =
callback My_Type.Cons
""")),
libDir);

var projDir = tempFolder.newFolder("Proj").toPath();
ProjectUtils.createProject(
"Proj",
"""
from local.Lib import call_method

callback cons =
cons "Name"

main =
call_method callback . to_text
""",
projDir);

ProjectUtils.testProjectRun(
projDir,
res -> {
assertThat(res.asString(), containsString("Cons 'Name'"));
});
}

@Test
public void canCallPrivateConstructor_ViaLambda() throws Exception {
var libDir = tempFolder.newFolder("Lib").toPath();
ProjectUtils.createProject(
"Lib",
Set.of(
new SourceModule(
QualifiedName.fromString("My_Type"),
"""
private

type My_Type
Cons name
"""),
new SourceModule(
QualifiedName.fromString("Main"),
"""
import project.My_Type.My_Type

call_method ~callback =
callback \\it -> My_Type.Cons it
""")),
libDir);

var projDir = tempFolder.newFolder("Proj").toPath();
ProjectUtils.createProject(
"Proj",
"""
from local.Lib import call_method

callback cons =
cons "Name"

main =
call_method callback . to_text
""",
projDir);

ProjectUtils.testProjectRun(
projDir,
res -> {
assertThat(res.asString(), containsString("Cons 'Name'"));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

import java.io.IOException;
import java.util.Set;
import org.enso.common.LanguageInfo;
import org.enso.common.MethodNames.Module;
import org.enso.pkg.QualifiedName;
import org.enso.test.utils.ContextUtils;
import org.enso.test.utils.ProjectUtils;
import org.enso.test.utils.SourceModule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

public class PrivateMethodAccessTest {
@Rule public TemporaryFolder tempFolder = new TemporaryFolder();

@Test
public void moduleDoesNotExposePrivateMethodsToPolyglot() {
try (var ctx = ContextUtils.createDefault()) {
Expand Down Expand Up @@ -45,4 +54,121 @@ public void typeDoesNotExposePrivateMethodsToPolyglot() {
assertThat("public method is exposed to polyglot", pubMethod.canExecute(), is(true));
}
}

@Test
public void canCallPrivateMethod_AsCallback() throws IOException {
var libDir = tempFolder.newFolder("Lib").toPath();
ProjectUtils.createProject(
"Lib",
Set.of(
new SourceModule(
QualifiedName.fromString("Mod"),
"""
private priv_method x = x
"""),
new SourceModule(
QualifiedName.fromString("Main"),
"""
from project.Mod import priv_method

call_method ~callback =
callback priv_method
""")),
libDir);

var projDir = tempFolder.newFolder("Proj").toPath();
ProjectUtils.createProject(
"Proj",
"""
from local.Lib import call_method

callback priv_method =
priv_method 42

main =
call_method callback
""",
projDir);

ProjectUtils.testProjectRun(
projDir,
res -> {
assertThat(res.asInt(), is(42));
});
}

@Test
public void canCallPrivateMethod_ViaLambda() throws IOException {
var libDir = tempFolder.newFolder("Lib").toPath();
ProjectUtils.createProject(
"Lib",
Set.of(
new SourceModule(
QualifiedName.fromString("Mod"),
"""
private priv_method =
42
"""),
new SourceModule(
QualifiedName.fromString("Main"),
"""
from project.Mod import priv_method

call_method ~callback =
callback \\_ -> priv_method
""")),
libDir);

var projDir = tempFolder.newFolder("Proj").toPath();
ProjectUtils.createProject(
"Proj",
"""
from local.Lib import call_method

callback priv_method =
# Here, we actually call the lambda from `local.Lib.Main.call_method`
priv_method 42

main =
call_method callback
""",
projDir);

ProjectUtils.testProjectRun(
projDir,
res -> {
assertThat(res.asInt(), is(42));
});
}

@Test
public void canCallPrivateMethod_UnresolvedSymbol() throws IOException {
var libDir = tempFolder.newFolder("Lib").toPath();
ProjectUtils.createProject(
"Lib", """
apply obj func =
func obj
""", libDir);

var projDir = tempFolder.newFolder("Proj").toPath();
ProjectUtils.createProject(
"Proj",
"""
from local.Lib import apply

type My_Type
private Cons value

main =
mt = My_Type.Cons 42
apply mt .value
""",
projDir);

ProjectUtils.testProjectRun(
projDir,
res -> {
assertThat(res.asInt(), is(42));
});
}
}
Loading
Loading