Repository: tact-lang/tact

Issue:
Forbid calling an extension function for type T on an expression with type T?
Example:
```tact
contract Test {
    addr: Address?;

    receive() {
        if (self.addr.isNotNone()) {
            throw(10);
        }
    }
}

inline extends fun isNotNone(self: Address): Bool { return self.asSlice().preloadUint(2) != 0 }
```


Hints from issue comments:

Solution patch:
diff --git a/dev-docs/CHANGELOG.md b/dev-docs/CHANGELOG.md
index 0bfbb72d..c9614e24 100644
--- a/dev-docs/CHANGELOG.md
+++ b/dev-docs/CHANGELOG.md
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [fix] Compiler now shows a more informative error message for fields with unsupported trait types: PR [#2695](https://github.com/tact-lang/tact/pull/2695)
 - [fix] Compiler now correctly generates code for functions with several wildcard parameters: PR [#2703](https://github.com/tact-lang/tact/pull/2703)
 - [fix] Compiler now checks that "override" functions and constants have a virtual or abstract modifier in the parent trait: PR [#2700](https://github.com/tact-lang/tact/pull/2700)
+- [fix] Compiler now throws an error if a non-optional method is called on an optional type: PR [#2770](https://github.com/tact-lang/tact/pull/2770)
 - [fix] Generated TypeScript wrappers now export all functions for serialization/deserialization: PR [#2706](https://github.com/tact-lang/tact/pull/2706)
 - [fix] Processing of `null` values of optional types in the `dump` builtin: PR [#2730](https://github.com/tact-lang/tact/pull/2730)
 
diff --git a/src/types/__snapshots__/resolveStatements.spec.ts.snap b/src/types/__snapshots__/resolveStatements.spec.ts.snap
index df228bd7..749ead6e 100644
--- a/src/types/__snapshots__/resolveStatements.spec.ts.snap
+++ b/src/types/__snapshots__/resolveStatements.spec.ts.snap
@@ -81,6 +81,15 @@ exports[`resolveStatements should fail statements for bounced-type-is-smaller 1`
 "
 `;
 
+exports[`resolveStatements should fail statements for call-non-optional-method-on-optional 1`] = `
+"<unknown>:10:13: Cannot call method "isNotNone" on an expression of type "Address?" (optional Address) without unwrapping it with "!!" first
+   9 |     receive() {
+> 10 |         if (self.addr.isNotNone()) {
+                   ^~~~~~~~~~~~~~~~~~~~~
+  11 |             throw(10);
+"
+`;
+
 exports[`resolveStatements should fail statements for codeOf 1`] = `
 "<unknown>:7:9: Type mismatch: "Cell" is not assignable to "Int"
   6 |     init() {
diff --git a/src/types/resolveExpression.ts b/src/types/resolveExpression.ts
index 4367b7e6..0448cc79 100644
--- a/src/types/resolveExpression.ts
+++ b/src/types/resolveExpression.ts
@@ -643,6 +643,18 @@ function resolveCall(
                 checkParameterType(exp.args[i]!, a, ctx);
             }
 
+            if (f.self) {
+                const selfIsOptional =
+                    f.self.kind === "ref" ? f.self.optional : false;
+
+                if (src.optional && !selfIsOptional) {
+                    throwCompilationError(
+                        `Cannot call method ${idTextErr(exp.method)} on an expression of type "${src.name}?" (optional ${src.name}) without unwrapping it with "!!" first`,
+                        exp.loc,
+                    );
+                }
+            }
+
             return registerExpType(ctx, exp, f.returns);
         }
 
diff --git a/src/types/stmts-failed/call-non-optional-method-on-optional.tact b/src/types/stmts-failed/call-non-optional-method-on-optional.tact
new file mode 100644
index 00000000..16bb134d
--- /dev/null
+++ b/src/types/stmts-failed/call-non-optional-method-on-optional.tact
@@ -0,0 +1,16 @@
+primitive Address;
+primitive Bool;
+
+trait BaseTrait {}
+
+contract Test {
+    addr: Address?;
+
+    receive() {
+        if (self.addr.isNotNone()) {
+            throw(10);
+        }
+    }
+}
+
+inline extends fun isNotNone(self: Address): Bool { return true }



Test patch:
diff --git a/src/test/e2e-slow/map-property-tests/map-properties.tact.template b/src/test/e2e-slow/map-property-tests/map-properties.tact.template
index 2d4d9d7e..11d4ad6b 100644
--- a/src/test/e2e-slow/map-property-tests/map-properties.tact.template
+++ b/src/test/e2e-slow/map-property-tests/map-properties.tact.template
@@ -108,7 +108,7 @@ fun size(m: map<KEY_TYPE_PLACEHOLDER, VAL_TYPE_PLACEHOLDER>): Int {
 
 asm fun castToMap(c: Cell): map<Int as uint8, Bool> {}
 
-asm extends fun asMap(self: Cell): map<KEY_TYPE_PLACEHOLDER, VAL_TYPE_PLACEHOLDER> {}
+asm extends fun asMap(self: Cell?): map<KEY_TYPE_PLACEHOLDER, VAL_TYPE_PLACEHOLDER> {}
 
 contract MapPropertiesTester {
     const ExitCodeEqualKeys: Int = 1024;
@@ -377,7 +377,7 @@ contract MapPropertiesTester {
                      .endCell());
         return
             m1 != m2 &&
-            m1.deepEquals(m2); 
+            m1.deepEquals(m2);
     }
 
     // Property: (m1 == m2) => m1.asCell() == m2.asCell()