From 5145b2afc2b788472ca49885140a177e28956236 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sun, 26 Oct 2025 20:29:52 +0800 Subject: [PATCH 01/10] :arrow_up: stable --- .github/workflows/get-rust-versions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/get-rust-versions.yml b/.github/workflows/get-rust-versions.yml index 998c4c529..8329d9097 100644 --- a/.github/workflows/get-rust-versions.yml +++ b/.github/workflows/get-rust-versions.yml @@ -18,7 +18,7 @@ on: value: ${{ jobs.get_versions.outputs.matrix }} env: - STABLE: "1.87.0" + STABLE: "1.88.0" NIGHTLY: "nightly-2024-11-08" OLD: "1.82.0" From 7646a8eadc8df9acb6921f98b9ed345c4a54fc89 Mon Sep 17 00:00:00 2001 From: Zero King Date: Thu, 30 Oct 2025 20:44:56 +0800 Subject: [PATCH 02/10] chore: add CLAUDE.md --- CLAUDE.md | 276 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..6bfbbed9d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,276 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a community fork of the IntelliJ Rust plugin - an open-source plugin providing Rust language support for JetBrains IDEs. The plugin is built with Kotlin and targets IntelliJ Platform 2025.2 (platform version 252). It does not rely on JetBrains' proprietary "rust-capable" plugins, making it installable on any JetBrains IDE. + +**Important**: This should NOT be installed simultaneously with other Rust plugins or used in RustRover. + +## Build and Development Commands + +### Essential Commands + +```bash +# Build the plugin (creates distributable ZIP) +./gradlew :plugin:buildPlugin +# Output: plugin/build/distributions/intellij-rust-*.zip + +# Run development IDE with plugin installed +./gradlew :plugin:runIde +# By default runs IntelliJ IDEA (baseIDE=IU in gradle.properties) + +# Run all tests (5000+ tests) +./gradlew :test + +# Run tests for a specific module +./gradlew :idea:test +./gradlew :plugin:test + +# Generate lexer and parser (run after modifying .flex or .bnf files) +./gradlew :generateLexer +./gradlew :generateParser +``` + +### Testing + +```bash +# Run a single test class +./gradlew :test --tests "org.rust.lang.core.parser.RustParserTest" + +# Run tests with detailed output +./gradlew :test -PshowTestStatus=true + +# Run tests with standard streams visible +./gradlew :test -PshowStandardStreams=true +``` + +### Native Code Compilation + +```bash +# Build native helper (procedural macro expander) +# This is automatically triggered by prepareSandbox, but can be run manually: +cd native-helper && cargo build --release +# Disable with -PcompileNativeCode=false if needed +``` + +### Other Useful Tasks + +```bash +# Verify plugin compatibility +./gradlew :plugin:verifyPlugin + +# Resolve all dependencies (useful after gradle.properties changes) +./gradlew resolveDependencies +``` + +## Architecture Overview + +### Module Structure + +The plugin is organized into multiple Gradle submodules to separate platform-specific and feature-specific code: + +- **`:` (root)** - Core module with Rust language support (parser, PSI, type inference, name resolution) +- **`:plugin`** - Aggregator module for building/publishing the final plugin artifact +- **`:idea`** - IDEA-specific functionality (Java integration) +- **`:copyright`** - Integration with copyright plugin +- **`:coverage`** - Code coverage integration +- **`:duplicates`** - Duplicate code detection +- **`:grazie`** - Grammar checking integration +- **`:js`** - JavaScript interop +- **`:ml-completion`** - ML-based code completion integration +- **`:grammar-kit-fake-psi-deps`** - Support module for parser generation + +### Core Package Organization + +- **`org.rust.lang`** - Language core: parser, PSI, name resolution, type inference + - `lang.core.lexer` - Generated lexer from RustLexer.flex + - `lang.core.parser` - Generated parser from RustParser.bnf + - `lang.core.psi` - PSI (Program Structure Interface) elements + - `lang.core.stubs` - Stub-based indexes for fast navigation + - `lang.core.types.infer` - Type inference engine (modeled after rustc) + - `lang.core.resolve` - Name resolution (lazy, upward-walking, see NameResolution.kt) + +- **`org.rust.cargo`** - Cargo and rustup integration + - `cargo.project.model` - Project model (CargoProject, CargoWorkspace, Package, Target) + - `cargo.project.workspace` - Workspace management and cargo metadata parsing + - `cargo.toolchain` - Rustc/cargo toolchain interaction + +- **`org.rust.ide`** - IDE features built on lang/cargo + - `ide.intentions` - Quick actions (Alt+Enter) + - `ide.inspections` - Warnings and quick fixes + - `ide.navigation.goto` - Go to Symbol/Class functionality + +### Platform Version Support + +The plugin uses version-specific source directories to handle IntelliJ Platform API changes: +- `src/252/main/kotlin` - Platform 252 (2025.2) specific code +- `src/main/kotlin` - Platform-independent code + +Only one version's code is compiled based on `platformVersion` in gradle.properties. + +### Parser and PSI Generation + +The lexer and parser are generated from grammar files: +- **Lexer**: `src/main/grammars/RustLexer.flex` → generates `RustLexer.java` via JFlex +- **Parser**: `src/main/grammars/RustParser.bnf` → generates `RustParser.java` + PSI interfaces via Grammar-Kit + +**After modifying grammar files, you MUST run the generator tasks:** +```bash +./gradlew :generateLexer :generateParser +``` + +PSI elements use a mixin pattern: +- Generated interface: `RsStructItem` +- Generated implementation: `RsStructItemImpl` +- Hand-written mixin: `RsStructItemImplMixin` (for custom logic) + +### Project Model Concepts + +Understanding the Cargo project model is critical for many features: + +``` +[CargoProject] (corresponds to Cargo.toml) + ↓ +[CargoWorkspace] (from `cargo metadata`) + ↓ +[Package] (dependencies, name/version) + ↓ +[Target] (lib.rs, main.rs, etc. - the compilable units) +``` + +- **CargoProject**: Represents a Cargo.toml linked to the IDE project +- **CargoWorkspace**: Contains packages, acquired via `cargo metadata` +- **Package**: A dependency unit (has name/version, found in [dependencies]) +- **Target**: Compilable artifact (binary, lib, test) with a crate root file + +Key distinction: Dependencies in Cargo.toml are **Packages**. The `extern crate foo` refers to a library **Target**. + +### Name Resolution + +Differs from rustc's top-down approach: +- **Lazy resolution**: Walks PSI tree upwards from reference point +- **Caching**: Results cached via `CachedValuesManager`, invalidated on PSI changes +- Only resolves currently opened file and its dependencies (ignores most of crate) +- See `NameResolution.kt` for implementation + +### Type Inference + +Located in `org.rust.lang.core.types.infer`: +- Happens at function/constant level (`RsInferenceContextOwner`) +- Top-down walk constructs expression → type mapping (`RsInferenceResult`) +- Uses type variables and `UnificationTable` for deferred inference +- Handles generics/traits via `ObligationForest` constraint solving +- Modeled after rustc's type checking + +### Indexing and Stubs + +Stub trees enable fast navigation without parsing: +- Condensed AST with only resolve-critical info (declarations, not bodies) +- Binary format persisted to disk +- PSI dynamically switches between stub-based and AST-based implementations +- See `org.rust.lang.core.stubs` package + +Example: `RsModulesIndex` maps files to parent modules, handling #[path] attributes. + +### Native Helper + +The `native-helper` directory contains a Rust binary that provides procedural macro expansion support using rust-analyzer's proc-macro-srv. It's compiled during the build process and bundled with the plugin. + +## Testing Patterns + +### Test Structure + +Tests use fixture-driven approach: +1. Load initial state (fixture file or inline string) +2. Execute action +3. Verify final state + +Prefer triple-quoted Kotlin strings over separate fixture files: +```kotlin +fun `test something`() = checkFixByText(""" + fn foo() { + /*caret*/ + } +""", """ + fn foo() { + // fixed + } +""") +``` + +Use `` marker for cursor position in fixtures. + +### Running Tests + +- Test files: `src/test/kotlin/` +- Test resources: `src/test/resources/` (less preferred) +- IntelliJ run configurations: `Test`, `RunIDEA`, `RunCLion` + +## Commit Message Conventions + +Prefix commits with tags describing the change area: + +- `GRAM` - Grammar (.bnf) changes +- `PSI` - PSI changes +- `RES` - Name resolution +- `TY` - Type inference +- `COMP` - Code completion +- `STUB` - Stubs/indexes +- `MACRO` - Macro expansion +- `FMT` - Formatter +- `ANN` - Annotators/error highlighting +- `INSP` - Inspections +- `INT` - Intentions +- `CARGO` - Cargo integration +- `T` - Tests +- `GRD` - Gradle/build + +Keep summary under 72 characters. + +## Important Development Notes + +### Java Version +- Requires Java 21 for development (VERSION_21 in build.gradle.kts) +- Kotlin 2.2.20 with API version 2.1 + +### Resource Variants +The plugin has channel-specific resources: +- `src/main/resources-stable/` - Stable releases +- `src/main/resources-nightly/` - Nightly/dev builds + +Controlled by `publishChannel` property (dev, nightly, stable). + +### When Making PSI Changes +1. Modify `RustParser.bnf` and/or `RustLexer.flex` +2. Run `./gradlew :generateLexer :generateParser` +3. Implement mixins if custom logic needed +4. Add stub support if needed for indexing +5. Add tests + +### When Adding a New Inspection +- Extend from base inspection classes in `org.rust.ide.inspections` +- Add tests in `src/test/kotlin/org/rust/ide/inspections/` +- See PR #713 for example + +### When Adding a New Intention +- Extend from base intention classes in `org.rust.ide.intentions` +- Add tests in `src/test/kotlin/org/rust/ide/intentions/` +- See PR #318 for example + +## Key Configuration Files + +- `gradle.properties` - Platform version, IDE selection, build settings +- `gradle-252.properties` - Platform 252 specific versions and plugin dependencies +- `settings.gradle.kts` - Multi-module project structure +- `build.gradle.kts` - Main build logic, tasks, and dependencies + +## Documentation Resources + +- [ARCHITECTURE.md](ARCHITECTURE.md) - Detailed architecture documentation +- [CONTRIBUTING.md](CONTRIBUTING.md) - Development environment setup and contribution guidelines +- [MAINTAINING.md](MAINTAINING.md) - Maintainer-specific information +- IntelliJ Platform SDK: https://www.jetbrains.org/intellij/sdk/docs/ +- Custom Language Support: https://www.jetbrains.org/intellij/sdk/docs/reference_guide/custom_language_support.html +- Grammar-Kit HOWTO: https://github.com/JetBrains/Grammar-Kit/blob/master/HOWTO.md From d511856cb391c7e11aa6740a572527e35892808a Mon Sep 17 00:00:00 2001 From: Zero King Date: Fri, 31 Oct 2025 00:15:45 +0800 Subject: [PATCH 03/10] chore: add unsafe_extern feature migration plan --- MIGRATION_PLAN.md | 1745 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1745 insertions(+) create mode 100644 MIGRATION_PLAN.md diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md new file mode 100644 index 000000000..0a6be7e77 --- /dev/null +++ b/MIGRATION_PLAN.md @@ -0,0 +1,1745 @@ +# Migration Plan: Unsafe Extern Blocks Support (RFC 3484) + +**Feature:** Support for `unsafe extern` blocks introduced in Rust 1.82 +**RFC:** [RFC 3484 - unsafe_extern_blocks](https://github.com/rust-lang/rfcs/blob/master/text/3484-unsafe-extern-blocks.md) +**Status:** Not Started +**Target Completion:** TBD + +## Executive Summary + +This migration adds support for Rust 1.82's `unsafe extern` blocks feature, which: +- Requires `unsafe` keyword on extern blocks in Edition 2024 +- Allows `safe`/`unsafe` qualifiers on items within extern blocks +- Maintains backward compatibility with pre-2024 editions + +**Total Estimated Effort:** 3.5-4.5 days (26.5-36.5 hours) + +**Note:** Effort estimates reflect extending existing PSI types (following the plugin's 2016 architectural pattern) rather than creating new dedicated types. + +--- + +## Phase 1: Grammar Changes (GRAM) + +**Estimated Effort:** 2.5-3.5 hours +**Prerequisites:** None +**Branch Suggestion:** `feature/unsafe-extern-grammar` + +### 1.1 Handle `safe` as Contextual Keyword + +**Important:** The `safe` keyword is a **contextual keyword** in Rust, meaning it's only treated as a keyword in specific grammar contexts (inside `extern` blocks) and can still be used as a regular identifier elsewhere in code. + +**Implementation Approach:** + +In Grammar-Kit based parsers, contextual keywords are handled by: +1. **NOT adding them as hard keywords to the lexer** - they remain `IDENTIFIER` tokens +2. Using the identifier token in grammar rules and matching specific text values where needed +3. The parser generator handles the context-specific recognition + +**Why No Lexer Changes:** +- Hard keywords (like `fn`, `let`, `unsafe`) are always keywords and cannot be identifiers +- Contextual keywords (like `safe`) are identifiers that have special meaning only in certain contexts +- If we made `safe` a hard keyword, existing code using `safe` as a variable/function name would break + +**No Changes Required to RustLexer.flex** + +**Effort:** 5 minutes (understanding only) + +--- + +### 1.2 Update Parser Grammar + +**File:** `src/main/grammars/RustParser.bnf` + +#### 1.2.1 Understanding Contextual Keywords in Grammar + +**Approach:** Use identifier matching for `safe` keyword in grammar rules. + +In Grammar-Kit, contextual keywords are referenced using quoted strings `'safe'` in the BNF grammar. The parser generator creates rules that match an IDENTIFIER token with the specific text "safe" only in the contexts where the rule is used. + +**Example Pattern:** +```bnf +// This matches the identifier "safe" only in this specific rule +safe ::= 'safe' +``` + +**No Token Section Changes Needed** - The token will be an IDENTIFIER at lexer level, but grammar rules will match it specifically by text. + +**Effort:** 5 minutes (understanding) + +--- + +#### 1.2.2 Update ForeignModItem Rule (NO CHANGES NEEDED) + +**Location:** Line 755 + +**Current:** +```bnf +upper ForeignModItem ::= unsafe? ExternAbi ForeignModBody +``` + +**Analysis:** Grammar already supports optional `unsafe` prefix. No structural changes needed, only semantic interpretation changes. + +**Effort:** 0 minutes + +--- + +#### 1.2.3 Add Safety Qualifier Support to Existing Function/Constant Rules + +**Location:** Line 536 (Function rule) and Line 653 (Constant rule) + +**Approach:** Extend existing `RsFunction` and `RsConstant` types with safety qualifier support. + +This follows the plugin's established architectural pattern from 2016: +- Commit `8adc969be` (Dec 29, 2016): "(GRAM): foreign_static is constant" - unified foreign statics with RsConstant +- Commit `76e26cd09` (Dec 28, 2016): "(GRAM): add function element" - explicitly states: "The plan is to use it for **all kinds of functions**: freestanding, foreign, both types of methods" + +This same pattern has been consistently applied to all function modifiers (`async`, `const`, `unsafe`) and similar variants (`struct/union`, `const/static/mut static`). + +**Grammar Changes:** + +First, add a helper rule for the contextual keyword (near other contextual keyword rules like `try`, `gen`, `raw`): +```bnf +private safe ::= <> +``` + +This invokes a meta-rule function that will be defined in `RustParserUtil.kt`. + +Then update the Function and Constant rules: +```bnf +// Line 536: Update existing Function rule +upper Function ::= async? const? unsafe? safe? ExternAbi? + fn identifier + TypeParameterList? + FnParameters + RetType? + WhereClause? + (';' | ShallowBlock) + +// Line 653: Update existing Constant rule +upper Constant ::= (('safe' | 'unsafe') static mut? | static mut? | const) + (identifier | '_') + TypeAscription? + [ '=' AnyExpr ] ';' +``` + +**Note:** In the Constant rule, we use literal `'safe'` and `'unsafe'` because the first appearance in the alternative handles the contextual matching. For Function, we use the `safe` rule which applies the contextual keyword logic. + +**Grammar Notes:** +- The `safe` rule uses Grammar-Kit's meta-rule syntax `<>` to call a parser utility function +- In the Constant rule, safety qualifiers only apply to `static` items (not `const`) +- This is semantically correct: `safe const` and `unsafe const` are invalid in Rust +- Pin point remains at `pin = 2` to allow backtracking when parsing `const fn` (tries Constant first, backtracks to Function) +- The contextual keyword approach ensures `safe` can still be used as a regular identifier in other contexts + +**Architecture:** Foreign items continue using `RsFunction` and `RsConstant` PSI types. Differentiation happens via: +1. **Parent context**: Check if `parent is RsForeignModItem` +2. **Owner pattern**: Existing `RsAbstractableOwner.Foreign` enum case +3. **Bit flags**: Add `IS_SAFE_MASK` to existing stub flags field (32-bit flags with 22 bits still available) + +**Benefits of This Approach:** +- Consistent with plugin's 2016 architectural foundation +- Matches existing modifier patterns throughout the codebase +- Minimal code addition (~300 lines vs ~2,000 for separate types) +- No breaking changes to existing APIs +- Centralized maintenance - changes apply to all function contexts +- Zero-cost stub storage - fits in existing flags field + +**Effort:** 30-40 minutes (grammar changes only) + +--- + +### 1.2.4 Add Parser Utility Function for Contextual Keyword + +**File:** `src/main/kotlin/org/rust/lang/core/parser/RustParserUtil.kt` +**Location:** Near other contextual keyword functions (search for `tryKeyword`, `rawKeyword`) + +**Add Function:** +```kotlin +@JvmStatic +fun safeKeyword(b: PsiBuilder, level: Int): Boolean = contextualKeyword(b, "safe", SAFE) +``` + +**How It Works:** +- When the parser encounters an `IDENTIFIER` token with text "safe" in a position where the `safe` grammar rule is used +- The `contextualKeyword()` function checks the token type and text +- It remaps the token from `IDENTIFIER` to `SAFE` element type using `b.remapCurrentToken(SAFE)` +- This allows `safe` to be recognized as a keyword in specific grammar contexts while remaining an identifier elsewhere + +**Required Token Type:** +The `SAFE` element type needs to exist. Add it to `RsElementTypes.kt` or ensure it's available: +```kotlin +@JvmField val SAFE = RsTokenType("SAFE") +``` + +**Effort:** 15 minutes + +--- + +### 1.2.5 Architectural Rationale: The "Owner Pattern" + +**Background:** Understanding this pattern is critical for implementing safety qualifiers correctly. + +The plugin uses a **context-over-type** architecture throughout: + +**Pattern 1: RsAbstractableOwner for Context Detection** + +```kotlin +// From: src/main/kotlin/org/rust/lang/core/psi/ext/RsAbstractable.kt +sealed class RsAbstractableOwner { + object Free : RsAbstractableOwner() + object Foreign : RsAbstractableOwner() // ← Used for extern blocks + class Trait(val trait: RsTraitItem) : RsAbstractableOwner() + class Impl(val impl: RsImplItem, val isInherent: Boolean) : RsAbstractableOwner() +} +``` + +**Pattern 2: Contextual Properties Using Owner** + +```kotlin +// From: RsFunction.kt:165-183 (already checks parent context) +val RsFunction.isActuallyUnsafe: Boolean + get() { + if (isUnsafe) return true + val context = context + return if (context is RsForeignModItem) { // ← Parent check + // functions inside extern blocks are unsafe by default + when { + context.queryAttributes.hasAttribute("wasm_bindgen") -> false + else -> true + } + } else { + false + } + } +``` + +**Pattern 3: Consistent Use of Bit Flags for Modifiers** + +| Rust Feature | PSI Type | Differentiation Method | +|--------------|----------|------------------------| +| `fn` / `async fn` / `const fn` / `unsafe fn` | `RsFunction` | Bit flags: `ASYNC_MASK`, `CONST_MASK`, `UNSAFE_MASK` | +| `struct` / `union` | `RsStructItem` | Bit flag: `IS_UNION_MASK` | +| `const` / `static` / `static mut` | `RsConstant` | Bit flags + kind enum | +| Free fn / trait fn / impl fn / foreign fn | `RsFunction` | `owner` property (sealed class) | +| `safe fn` / `unsafe fn` in extern | `RsFunction` | Bit flag: `SAFE_MASK` + owner context | +| `safe static` / `unsafe static` in extern | `RsConstant` | Bit flags: `IS_SAFE_MASK`, `IS_UNSAFE_MASK` | + +**Historical Example:** The 2018 unification of trait types (commit `c7d2cd2c4`): +- **Before:** Three separate PSI types (`ImplicitDynTraitType`, `ExplicitDynTraitType`, `ImplTraitType`) +- **After:** Single `TraitType` with contextual detection +- **Reason:** Semantic similarity + code duplication + +**Implementation for This Feature:** + +The new `isSafe` property will integrate seamlessly: + +```kotlin +val RsFunction.isSafe: Boolean + get() = greenStub?.isSafe ?: (safe != null) + +val RsFunction.effectiveSafety: Safety + get() = when (owner) { + is RsAbstractableOwner.Foreign -> { + when { + isSafe -> Safety.Safe // ← Explicit safe + isUnsafe -> Safety.Unsafe // ← Explicit unsafe + else -> Safety.Unsafe // ← Default in foreign blocks + } + } + else -> if (isUnsafe) Safety.Unsafe else Safety.Safe + } +``` + +**Result:** Zero changes needed to: +- Type inference infrastructure +- Name resolution +- Completion +- Navigation +- Most IDE features + +--- + +### 1.3 Generate Parser and Lexer + +**Command:** +```bash +./gradlew :generateLexer :generateParser +``` + +**Success Criteria:** +- [ ] Task completes without errors +- [ ] Generated files updated in `src/gen/` +- [ ] No compilation errors after generation +- [ ] The `SAFE` token type is available in generated code + +**Effort:** 15 minutes (including build time) + +--- + +### Phase 1 Checkpoint + +**Verification Steps:** +1. Run `./gradlew :generateLexer :generateParser` +2. Run `./gradlew :plugin:buildPlugin` - should compile +3. Manually inspect generated parser code +4. Test that `safe` keyword is recognized in extern contexts while remaining usable as identifier elsewhere + +**Success Criteria:** +- [ ] All generation tasks complete successfully +- [ ] Project compiles without errors +- [ ] Parser accepts `safe` as contextual keyword in extern item contexts +- [ ] Parser accepts `unsafe extern {}` syntax +- [ ] `safe` can still be used as a regular identifier (variable names, etc.) + +**Blocking Issues:** None expected + +--- + +## Phase 2: PSI & Stub Changes (PSI, STUB) + +**Estimated Effort:** 4-5 hours +**Prerequisites:** Phase 1 complete +**Branch Suggestion:** `feature/unsafe-extern-psi` (or continue from Phase 1) + +⚠️ **CRITICAL:** This phase includes stub format changes requiring version bump and full re-indexing. + +--- + +### 2.1 Extend RsForeignModStub + +**File:** `src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt` +**Location:** Search for `class RsForeignModStub` + +#### 2.1.1 Update Stub Class + +**Current:** +```kotlin +class RsForeignModStub( + parent: StubElement<*>?, elementType: IStubElementType<*, *>, + override val flags: Int, + override val procMacroInfo: RsProcMacroStubInfo?, + val abi: String?, +) : RsAttrProcMacroOwnerStubBase(parent, elementType) +``` + +**Updated:** +```kotlin +class RsForeignModStub( + parent: StubElement<*>?, elementType: IStubElementType<*, *>, + override val flags: Int, + override val procMacroInfo: RsProcMacroStubInfo?, + val abi: String?, + val isUnsafe: Boolean // NEW FIELD +) : RsAttrProcMacroOwnerStubBase(parent, elementType) +``` + +**Effort:** 10 minutes + +--- + +#### 2.1.2 Update Serialization (deserialize) + +**Location:** In RsForeignModStub.Type object, deserialize method + +**Updated:** +```kotlin +override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>?) = + RsForeignModStub( + parentStub, this, + dataStream.readUnsignedByte(), + RsProcMacroStubInfo.deserialize(dataStream), + abi = dataStream.readNameString(), + isUnsafe = dataStream.readBoolean() // NEW + ) +``` + +**Effort:** 5 minutes + +--- + +#### 2.1.3 Update Serialization (serialize) + +**Location:** In RsForeignModStub.Type object, serialize method + +**Updated:** +```kotlin +override fun serialize(stub: RsForeignModStub, dataStream: StubOutputStream) = + with(dataStream) { + writeByte(stub.flags) + RsProcMacroStubInfo.serialize(stub.procMacroInfo, dataStream) + writeName(stub.abi) + writeBoolean(stub.isUnsafe) // NEW + } +``` + +**Effort:** 5 minutes + +--- + +#### 2.1.4 Update Stub Creation + +**Location:** In RsForeignModStub.Type object, createStub method + +**Updated:** +```kotlin +override fun createStub(psi: RsForeignModItem, parentStub: StubElement<*>?): RsForeignModStub { + val flags = RsAttributeOwnerStub.extractFlags(psi) + val procMacroInfo = RsAttrProcMacroOwner Stub.extractTextAndOffset(flags, psi) + return RsForeignModStub( + parentStub, this, + flags, procMacroInfo, + abi = psi.abi, + isUnsafe = psi.unsafe != null // NEW + ) +} +``` + +**Effort:** 5 minutes + +--- + +### 2.2 Bump Stub Version + +**File:** `src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt` +**Location:** Top of file, search for `private const val STUB_VERSION` + +**Current:** +```kotlin +private const val STUB_VERSION = 234 +``` + +**Updated:** +```kotlin +private const val STUB_VERSION = 235 // Bump for unsafe extern blocks support +``` + +**Impact:** Forces complete re-indexing of all Rust projects + +**Effort:** 2 minutes + +--- + +### 2.3 Update RsForeignModItem PSI Extension + +**File:** `src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt` +**Location:** After existing properties (after abi property, before RsForeignModItemImplMixin class) + +**Add Property:** +```kotlin +val RsForeignModItem.isUnsafe: Boolean + get() = greenStub?.isUnsafe ?: (unsafe != null) +``` + +**Effort:** 10 minutes + +--- + +### 2.4 Add Safety Qualifier Support for Foreign Items + +**Approach:** Extend existing `RsFunction` with `isSafe` flag (following the plugin's 2016 architectural pattern). + +#### 2.4.1 Update RsFunctionStub with SAFE_MASK Flag + +**File:** `src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt` +**Location:** Search for `class RsFunctionStub` + +**Current Structure:** +```kotlin +class RsFunctionStub( + parent: StubElement<*>?, elementType: IStubElementType<*, *>, + override val name: String?, + val abiName: String?, + override val flags: Int, // ← Uses bit flags (32 bits available) + override val procMacroInfo: RsProcMacroStubInfo?, +) +``` + +**Add Property (in RsFunctionStub class):** +```kotlin +val isSafe: Boolean get() = BitUtil.isSet(flags, SAFE_MASK) +``` + +**Add to Companion Object:** +```kotlin +companion object : BitFlagsBuilder(FunctionStubAttrFlags, INT) { + private val ABSTRACT_MASK: Int = nextBitMask() + private val CONST_MASK: Int = nextBitMask() + private val UNSAFE_MASK: Int = nextBitMask() + private val EXTERN_MASK: Int = nextBitMask() + private val VARIADIC_MASK: Int = nextBitMask() + private val ASYNC_MASK: Int = nextBitMask() + private val HAS_SELF_PARAMETER_MASK: Int = nextBitMask() + private val PREFERRED_BRACES: Int = run { + val mask = nextBitMask() + nextBitMask() // second bit + mask.countTrailingZeroBits() + } + private val SAFE_MASK: Int = nextBitMask() // ← NEW (add after PREFERRED_BRACES) +} +``` + +**Note:** SAFE_MASK must be added after PREFERRED_BRACES (which uses 2 bits) to avoid conflicts with existing bit assignments. + +**Update createStub Method (in RsFunctionStub.Type object):** +```kotlin +override fun createStub(psi: RsFunction, parentStub: StubElement<*>?): RsFunctionStub { + // ... attribute extraction logic ... + + var flags = RsAttributeOwnerStub.extractFlags(attrs, FunctionStubAttrFlags) + flags = BitUtil.set(flags, ABSTRACT_MASK, block == null) + flags = BitUtil.set(flags, CONST_MASK, psi.isConst) + flags = BitUtil.set(flags, UNSAFE_MASK, psi.isUnsafe) + flags = BitUtil.set(flags, EXTERN_MASK, psi.isExtern) + flags = BitUtil.set(flags, VARIADIC_MASK, psi.isVariadic) + flags = BitUtil.set(flags, SAFE_MASK, psi.isSafe) // ← NEW (add with other flags) + flags = BitUtil.set(flags, ASYNC_MASK, psi.isAsync) + flags = BitUtil.set(flags, HAS_SELF_PARAMETER_MASK, psi.hasSelfParameters) + + // ... rest of method including PREFERRED_BRACES handling ... +} +``` + +**Effort:** 15 minutes + +--- + +#### 2.4.2 Add Safe Keyword Accessor to RsFunction + +**File:** `src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt` +**Location:** After existing keyword properties (near rawReturnType, before valueParameters) + +**Add:** +```kotlin +val RsFunction.isSafe: Boolean + get() = greenStub?.isSafe ?: (safe != null) +``` + +**Effort:** 5 minutes + +--- + +### 2.5 Add Safety Qualifier Support for Foreign Statics + +**Approach:** Extend existing `RsConstant` with `isSafe` and `isUnsafe` flags (mirrors RsFunction approach). + +#### 2.5.1 Update RsConstantStub with Safety Flags + +**File:** `src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt` +**Location:** Search for `class RsConstantStub` + +**Add Properties (in RsConstantStub class):** +```kotlin +val isSafe: Boolean get() = BitUtil.isSet(flags, IS_SAFE_MASK) +val isUnsafe: Boolean get() = BitUtil.isSet(flags, IS_UNSAFE_MASK) +``` + +**Add to Companion Object:** +```kotlin +companion object : BitFlagsBuilder(ConstantStubAttrFlags, INT) { + private val IS_MUT_MASK: Int = nextBitMask() + private val IS_CONST_MASK: Int = nextBitMask() + private val IS_SAFE_MASK: Int = nextBitMask() // ← NEW + private val IS_UNSAFE_MASK: Int = nextBitMask() // ← NEW +} +``` + +**Important:** Use `INT` as the storage limit (not `BYTE`) because RsConstantStub needs more than 8 bits total: +- 5 flags from CommonStubAttrFlags (inherited via ConstantStubAttrFlags) +- 2 existing flags (IS_MUT_MASK, IS_CONST_MASK) +- 2 new safety flags (IS_SAFE_MASK, IS_UNSAFE_MASK) +- Total: 9 flags requiring INT (32-bit) storage + +This follows the same pattern as RsFunctionStub where attribute-level flags use BYTE limit while stub-level storage uses INT. + +**Update createStub Method (in RsConstantStub.Type object):** +```kotlin +override fun createStub(psi: RsConstant, parentStub: StubElement<*>?): RsConstantStub { + var flags = RsAttributeOwnerStub.extractFlags(psi) + flags = BitUtil.set(flags, IS_MUT_MASK, psi.isMut) + flags = BitUtil.set(flags, IS_CONST_MASK, psi.isConst) + flags = BitUtil.set(flags, IS_SAFE_MASK, psi.safe != null) // ← NEW + flags = BitUtil.set(flags, IS_UNSAFE_MASK, psi.unsafe != null) // ← NEW + // ... rest of method +} +``` + +**Effort:** 15 minutes + +--- + +#### 2.5.2 Add Safety Accessors to RsConstant + +**File:** `src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt` +**Location:** After existing properties (near isConst, before kind property) + +**Add:** +```kotlin +val RsConstant.isSafe: Boolean get() = greenStub?.isSafe ?: (safe != null) + +val RsConstant.isUnsafe: Boolean get() = greenStub?.isUnsafe ?: (unsafe != null) +``` + +**Effort:** 5 minutes + +--- + +### Phase 2 Checkpoint + +**Verification Steps:** +1. Run `./gradlew :plugin:buildPlugin` - should compile +2. Run unit tests: `./gradlew :test --tests "*Stub*"` +3. Verify stub version incremented +4. Check that old stub files are invalidated + +**Success Criteria:** +- [ ] RsForeignModStub includes `isUnsafe` field +- [ ] Serialization/deserialization updated +- [ ] Stub version bumped to 235 +- [ ] RsForeignModItem.isUnsafe property works +- [ ] All stub tests pass +- [ ] Project compiles without errors + +**Blocking Issues:** +- May need to handle stub migration for existing indexed projects + +--- + +## Phase 3: Parser & Stub Tests (T) + +**Estimated Effort:** 4-5 hours +**Prerequisites:** Phase 1 & 2 complete +**Branch:** Continue from Phase 2 + +**Note:** Following TDD, these tests should ideally be written BEFORE implementation, but documenting here for tracking. + +--- + +### 3.1 Create Parser Test Fixture + +**File:** `src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.rs` + +**Content:** +```rust +// Edition 2024 syntax - extern blocks require unsafe +unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + pub unsafe fn strlen(p: *const u8) -> usize; + pub fn default_unsafe(ptr: *mut u8); // defaults to unsafe + + pub safe static SAFE_CONSTANT: i32; + pub unsafe static UNSAFE_PTR: *const u8; + static DEFAULT_UNSAFE: *mut u8; // defaults to unsafe + + pub type OpaqueType; +} + +// Multiple ABIs +unsafe extern { + safe fn no_abi_safe(); +} + +unsafe extern "system" { + unsafe fn system_unsafe(); +} + +unsafe extern "Rust" { + fn rust_abi_fn(); +} + +// Legacy syntax (pre-2024) - still valid without unsafe +extern "C" { + fn legacy_function(); + static LEGACY_STATIC: i32; +} + +// Nested in modules +mod ffi { + unsafe extern "C" { + pub safe fn nested_safe(); + } +} + +// With attributes +#[link(name = "mylib")] +unsafe extern "C" { + #[link_name = "actual_name"] + safe fn attributed_fn(); +} +``` + +**Effort:** 30 minutes + +--- + +### 3.2 Add Parser Test Case + +**File:** `src/test/kotlin/org/rust/lang/core/parser/RsCompleteParsingTestCase.kt` +**Location:** Add to test class (around line 29) + +**Add:** +```kotlin +fun `test unsafe extern blocks`() = doTest(true) +``` + +**Effort:** 5 minutes + +--- + +### 3.3 Update Existing Parser Fixture + +**File:** `src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.rs` +**Location:** Lines 20-21 + +**Current:** +```rust +unsafe extern {} // semantically invalid +pub unsafe extern {} // semantically invalid +``` + +**Updated:** +```rust +unsafe extern {} // semantically valid in edition 2024 +pub unsafe extern {} // semantically valid in edition 2024 +``` + +**Effort:** 5 minutes + +--- + +### 3.4 Create Stub Test + +**File:** `src/test/kotlin/org/rust/lang/core/stubs/RsForeignModStubTest.kt` (NEW FILE) + +**Test Structure:** Inherit from `RsTestBase` and use the `doStubTreeTest()` helper pattern for stub validation. + +**Implementation Guidance:** + +1. **Create a helper method** `doStubTreeTest()` that: + - Takes Rust code and expected stub tree structure as strings + - Uses `fileTreeFromText()` to create test file + - Loads stub tree via `StubTreeLoader` + - Compares actual stub tree with expected structure using `DebugUtil.stubTreeToString()` + +2. **Write test cases covering:** + - **Basic unsafe extern block** - verify stub structure for `unsafe extern "C" { fn foo(); }` + - **Legacy safe extern block** - verify stub structure for `extern "C" { fn bar(); }` + - **Unsafe extern without explicit ABI** - verify `unsafe extern { fn baz(); }` works + - **Multiple items** - test extern block with multiple functions and statics + - **Inner attributes** - test extern block containing `#![allow(...)]` style attributes + +3. **Expected stub tree format:** +``` +RsFileStub + FOREIGN_MOD_ITEM:RsForeignModStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub +``` + +**Key Testing Points:** +- Verify stubs are created for all extern block variants +- Ensure inner attributes are properly captured in stub tree +- Test that multiple items (functions, statics) all appear in stub hierarchy +- Validate stub tree structure matches PSI hierarchy + +**Effort:** 1 hour + +--- + +### 3.5 Create Tests for Safe/Unsafe Function Qualifiers + +**File:** `src/test/kotlin/org/rust/lang/core/psi/RsForeignItemTest.kt` (NEW FILE) + +**Test Structure:** Inherit from `RsTestBase` and use the `InlineFile()` helper for creating test files. + +**Implementation Guidance:** + +1. **Use InlineFile() pattern** for test setup: + - Call `InlineFile("""...""")` with Rust code + - Access the file via `myFixture.file` + - Use `descendantsOfType()` to find elements + +2. **Write 6 test cases covering:** + - **Safe foreign function** - verify `isSafe` is true, `isActuallyUnsafe` is false + - **Unsafe foreign function** - verify explicit `unsafe` qualifier is recognized + - **Default (no qualifier) foreign function** - verify defaults to unsafe semantics + - **Safe foreign static** - verify `isSafe` is true, `isUnsafe` is false + - **Unsafe foreign static** - verify explicit `unsafe` qualifier on static + - **Default foreign static** - verify neither `isSafe` nor `isUnsafe` are true + +3. **Test Pattern Example:** +```kotlin +fun `test safe foreign function`() { + InlineFile(""" + unsafe extern "C" { + pub safe fn safe_fn(); + } + """) + val fn = myFixture.file.descendantsOfType().single() + assertTrue("Function should be marked as safe", fn.isSafe) + assertFalse("Safe function should not be actually unsafe", fn.isActuallyUnsafe) +} +``` + +**Key Testing Points:** +- Verify PSI properties (`isSafe`, `isUnsafe`) correctly detect keywords +- Test semantic analysis (`isActuallyUnsafe`) - note this may require Phase 4 implementation +- Cover both functions and statics with all three qualifier states (safe, unsafe, none) + +**Note:** The `test safe foreign function` case requires Phase 4's `isActuallyUnsafe` implementation to pass correctly. + +**Effort:** 1 hour + +--- + +### 3.6 Run Parser Tests + +**Command:** +```bash +./gradlew :test --tests "org.rust.lang.core.parser.RsCompleteParsingTestCase" +``` + +**Success Criteria:** +- [ ] `test unsafe extern blocks` passes +- [ ] `test extern block` passes with updated fixture +- [ ] No regressions in other parsing tests + +**Effort:** 15 minutes + fixes + +--- + +### 3.7 Run Stub Tests + +**Command:** +```bash +./gradlew :test --tests "org.rust.lang.core.stubs.RsForeignModStubTest" +./gradlew :test --tests "org.rust.lang.core.psi.RsForeignItemTest" +``` + +**Success Criteria:** +- [ ] RsForeignModStubTest: 4/4 tests pass +- [ ] RsForeignItemTest: 5/6 tests pass (1 blocked on Phase 4's `isActuallyUnsafe` implementation) +- [ ] Total: 9/10 tests passing +- [ ] No regressions in existing stub tests +- [ ] Stub serialization/deserialization works correctly + +**Effort:** 15 minutes + fixes + +--- + +### Phase 3 Checkpoint + +**Verification Steps:** +1. Parser test passes: `test unsafe extern blocks` +2. Stub tests pass: RsForeignModStubTest (4/4), RsForeignItemTest (5/6) +3. Test coverage includes edge cases (no ABI, various qualifiers) +4. Legacy syntax still parses correctly +5. Total test results: 9/10 passing, 1 requires Phase 4 + +**Success Criteria:** +- [ ] Parser accepts all unsafe extern syntax variants +- [ ] Stubs correctly store isUnsafe flag +- [ ] Safe/unsafe qualifiers on items are recognized +- [ ] 9/10 tests passing (1 test properly blocked on Phase 4 semantic analysis) +- [ ] No test regressions + +**Blocking Issues:** +- Parser test failures may indicate grammar issues (return to Phase 1) +- Stub test failures may indicate serialization issues (return to Phase 2) +- Expected: 1 test in RsForeignItemTest requires Phase 4's semantic analysis + +--- + +## Phase 4: Semantic Analysis (ANN, RES, TY) + +**Estimated Effort:** 6-8 hours +**Prerequisites:** Phase 1-3 complete +**Branch Suggestion:** `feature/unsafe-extern-semantics` or continue + +--- + +### 4.1 Update Function Unsafety Logic + +**File:** `src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt` +**Location:** Line 165 (isActuallyUnsafe property) + +**Current:** +```kotlin +val RsFunction.isActuallyUnsafe: Boolean + get() { + if (isUnsafe) return true + val context = context + return if (context is RsForeignModItem) { + // functions inside `extern` block are unsafe in most cases + when { + // #[wasm_bindgen] special case + context.queryAttributes.hasAttrWithName("wasm_bindgen") -> false + else -> true + } + } else { + false + } + } +``` + +**Updated:** +```kotlin +/** + * A function is unsafe if defined with `unsafe` modifier or if defined inside a certain `extern` + * block. But [RsFunction.isUnsafe] takes into account only `unsafe` modifier. [isActuallyUnsafe] + * takes into account both cases. + * + * For functions in unsafe extern blocks, items marked with `safe` are safe to call. + * Items without explicit marker default to unsafe. + */ +val RsFunction.isActuallyUnsafe: Boolean + get() { + // Explicit unsafe modifier always makes it unsafe + if (isUnsafe) return true + + val context = context + return if (context is RsForeignModItem) { + // Explicit safe marker makes it safe + if (isSafe) return false + + // If in unsafe extern block, default to unsafe + if (context.isUnsafe) return true + + // Legacy extern blocks (pre-2024): functions are unsafe unless special case + when { + // #[wasm_bindgen] is a procedural macro that removes the following + // extern block, so all functions inside it become safe. + context.queryAttributes.hasAttrWithName("wasm_bindgen") -> false + else -> true + } + } else { + false + } + } +``` + +**Effort:** 30 minutes + +--- + +### 4.2 Create Semantic Tests for Unsafety + +**File:** `src/test/kotlin/org/rust/lang/core/resolve/RsForeignFunctionSafetyTest.kt` (NEW FILE) + +**Content:** +```kotlin +package org.rust.lang.core.resolve + +import org.rust.lang.core.psi.ext.isActuallyUnsafe + +class RsForeignFunctionSafetyTest : RsCodeInsightTestBase() { + + fun `test safe foreign function is safe`() = checkByCode(""" + unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + } + + fn main() { + sqrt(4.0); // Should not require unsafe block + } + """) { + val fn = findElementInEditor("sqrt") + assertFalse(fn.isActuallyUnsafe) + } + + fun `test unsafe foreign function is unsafe`() = checkByCode(""" + unsafe extern "C" { + pub unsafe fn dangerous(); + } + + fn main() { + dangerous(); // Should require unsafe block + } + """) { + val fn = findElementInEditor("dangerous") + assertTrue(fn.isActuallyUnsafe) + } + + fun `test default foreign function is unsafe`() = checkByCode(""" + unsafe extern "C" { + pub fn implicit_unsafe(); + } + + fn main() { + implicit_unsafe(); // Should require unsafe block + } + """) { + val fn = findElementInEditor("implicit_unsafe") + assertTrue(fn.isActuallyUnsafe) + } + + fun `test wasm_bindgen special case still works`() = checkByCode(""" + #[wasm_bindgen] + extern "C" { + pub fn console_log(); + } + + fn main() { + console_log(); // Should not require unsafe + } + """) { + val fn = findElementInEditor("console_log") + assertFalse(fn.isActuallyUnsafe) + } +} +``` + +**Effort:** 1.5 hours + +--- + +### 4.3 Update Unsafe Expression Annotator + +**File:** `src/main/kotlin/org/rust/ide/annotator/RsUnsafeExpressionErrorAnnotator.kt` + +**Analysis Required:** Verify that annotator already uses `isActuallyUnsafe` - if so, no changes needed. If it uses a different check, update to use `isActuallyUnsafe`. + +**Search for:** Call expression checking logic + +**Expected:** Should automatically work with updated `isActuallyUnsafe` logic + +**Effort:** 30 minutes (investigation + potential fix) + +--- + +### 4.4 Create Tests for Unsafe Call Checking + +**File:** `src/test/kotlin/org/rust/ide/annotator/RsUnsafeExpressionErrorAnnotatorTest.kt` +**Location:** Add to existing test class + +**Add:** +```kotlin +fun `test safe foreign function call without unsafe block`() = checkByCode(""" + unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + } + + fn main() { + sqrt(4.0); // OK - no error + } +""") + +fun `test unsafe foreign function call requires unsafe block`() = checkByCode(""" + unsafe extern "C" { + pub unsafe fn dangerous(); + } + + fn main() { + dangerous(); + } +""") + +fun `test default foreign function call requires unsafe block`() = checkByCode(""" + unsafe extern "C" { + pub fn implicit_unsafe(); + } + + fn main() { + implicit_unsafe(); + } +""") + +fun `test safe foreign function call in unsafe block`() = checkByCode(""" + unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + } + + fn main() { + unsafe { sqrt(4.0); } // OK - safe functions don't need unsafe, but allowed + } +""") +``` + +**Effort:** 1 hour + +--- + +### 4.5 Run Semantic Tests + +**Command:** +```bash +./gradlew :test --tests "*RsUnsafe*" +./gradlew :test --tests "*RsForeignFunctionSafety*" +``` + +**Success Criteria:** +- [ ] Safe foreign functions can be called without unsafe +- [ ] Unsafe foreign functions require unsafe blocks +- [ ] Default (unmarked) foreign functions require unsafe blocks +- [ ] wasm_bindgen special case preserved + +**Effort:** 30 minutes + fixes + +--- + +### Phase 4 Checkpoint + +**Verification Steps:** +1. Run unsafe expression tests +2. Run foreign function safety tests +3. Manually test in RunIde +4. Check no regressions in existing unsafe tests + +**Success Criteria:** +- [ ] `isActuallyUnsafe` logic correctly handles all cases +- [ ] Annotator properly highlights unsafe calls +- [ ] Safe functions don't trigger unsafe errors +- [ ] Unsafe/default functions trigger appropriate errors +- [ ] No test regressions + +**Blocking Issues:** +- May need to update multiple annotators if logic is duplicated + +--- + +## Phase 5: Edition-Based Validation (ANN, INSP) + +**Estimated Effort:** 3.5-5.5 hours +**Prerequisites:** Phase 1-4 complete +**Branch:** Continue from Phase 4 + +--- + +### 5.1 Add Missing Unsafe on Extern Block Error + +**File:** `src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt` + +**Location:** Find the annotator method handling `RsForeignModItem` (search for a visitor method or add to existing checks) + +**Implementation Note:** Use the standard `isAtLeastEdition2024` property (defined in `RsElement.kt`) for edition detection. This follows the established pattern used throughout the codebase (e.g., `RsEdition2024KeywordsAnnotator`). + +**Add:** +```kotlin +private fun checkForeignModItem(holder: RsAnnotationHolder, element: RsForeignModItem) { + checkMissingUnsafeOnExternBlock(holder, element) +} + +private fun checkMissingUnsafeOnExternBlock(holder: RsAnnotationHolder, element: RsForeignModItem) { + // Only check for edition 2024+ + if (!element.isAtLeastEdition2024) return + + if (!element.isUnsafe) { + val externKeyword = element.externAbi.extern + holder.createErrorAnnotation( + externKeyword, + RsBundle.message("inspection.message.extern.blocks.must.be.unsafe.in.edition.2024"), + AddUnsafeToExternBlockFix(element) + ) + } +} +``` + +**Effort:** 30 minutes + +--- + +### 5.2 Create Quick Fix: Add Unsafe to Extern Block + +**File:** `src/main/kotlin/org/rust/ide/annotator/fixes/AddUnsafeToExternBlockFix.kt` (NEW FILE) + +**Content:** +```kotlin +package org.rust.ide.annotator.fixes + +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import org.rust.lang.core.psi.RsForeignModItem +import org.rust.lang.core.psi.RsPsiFactory +import org.rust.lang.core.psi.ext.startOffset + +class AddUnsafeToExternBlockFix( + element: RsForeignModItem +) : LocalQuickFixAndIntentionActionOnPsiElement(element) { + + override fun getText(): String = "Add `unsafe` keyword to extern block" + + override fun getFamilyName(): String = text + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + val foreignMod = startElement as? RsForeignModItem ?: return + val externKeyword = foreignMod.extern ?: return + + val unsafeKeyword = RsPsiFactory(project).createUnsafeKeyword() + foreignMod.addBefore(unsafeKeyword, externKeyword) + foreignMod.addAfter(RsPsiFactory(project).createWhitespace(" "), unsafeKeyword) + } +} +``` + +**Note:** May need to create helper in RsPsiFactory for creating keyword elements. + +**Effort:** 1 hour + +--- + +### 5.3 Add Inspection: Missing Unsafe on Extern (Opt-in for pre-2024) + +**File:** `src/main/kotlin/org/rust/ide/inspections/lints/RsMissingUnsafeOnExternInspection.kt` (NEW FILE) + +**Content:** +```kotlin +package org.rust.ide.inspections.lints + +import com.intellij.codeInspection.ProblemsHolder +import org.rust.cargo.project.workspace.CargoWorkspace +import org.rust.ide.annotator.fixes.AddUnsafeToExternBlockFix +import org.rust.ide.inspections.RsProblemsHolder +import org.rust.lang.core.psi.RsForeignModItem +import org.rust.lang.core.psi.RsVisitor + +/** + * Inspection for extern blocks missing unsafe keyword. + * This is an error in Edition 2024, but a warning/suggestion in earlier editions + * to prepare for migration. + */ +class RsMissingUnsafeOnExternInspection : RsLintInspection() { + + override fun getLint() = Lints.MissingUnsafeOnExtern + + override fun buildVisitor(holder: RsProblemsHolder, isOnTheFly: Boolean): RsVisitor = + object : RsVisitor() { + override fun visitForeignModItem(mod: RsForeignModItem) { + if (mod.isUnsafe) return + + // In edition 2024, this is handled as an error by RsErrorAnnotator + // Only show warning for pre-2024 editions + if (mod.isAtLeastEdition2024) return + + val externKeyword = mod.externAbi.extern + holder.registerProblem( + externKeyword, + "Extern block should be marked unsafe (required in Edition 2024)", + AddUnsafeToExternBlockFix(mod) + ) + } + } + + companion object Lints { + val MissingUnsafeOnExtern = RsLint( + "missing_unsafe_on_extern", + "Extern blocks should be marked unsafe", + RsLintLevel.WARN, + defaultLevel = RsLintLevel.ALLOW // Opt-in for pre-2024 + ) + } +} +``` + +**Effort:** 1.5 hours + +--- + +### 5.4 Register Inspection + +**File:** `src/main/resources/META-INF/plugin.xml` (or appropriate XML) + +**Add:** +```xml + +``` + +**Effort:** 15 minutes + +--- + +### 5.5 Add Safe Mutable Static Validation + +**File:** `src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt` + +**Add Method:** +```kotlin +private fun checkSafeMutableStatic(constant: RsConstant) { + // Only check statics inside extern blocks + val foreignMod = constant.context as? RsForeignModItem ?: return + + // Mutable statics cannot be marked safe + if (constant.isMut && constant.isSafe) { + val safeKeyword = constant.safe ?: return + holder.newAnnotation(HighlightSeverity.ERROR, + "Mutable static items in extern blocks cannot be marked safe") + .range(safeKeyword) + .withFix(RemoveSafeKeywordFix(constant)) + .create() + } +} +``` + +**Call from:** `visitConstant` method + +**Effort:** 30 minutes + +--- + +### 5.6 Create Edition-Based Tests + +**File:** `src/test/kotlin/org/rust/ide/annotator/RsErrorAnnotatorTest.kt` +**Location:** Add to existing test class + +**Add:** +```kotlin +@MockEdition(CargoWorkspace.Edition.EDITION_2024) +fun `test missing unsafe on extern in edition 2024`() = checkByCode(""" + extern "C" { + fn foo(); + } +""") + +@MockEdition(CargoWorkspace.Edition.EDITION_2024) +fun `test unsafe extern in edition 2024`() = checkByCode(""" + unsafe extern "C" { + fn foo(); + } +""") + +@MockEdition(CargoWorkspace.Edition.EDITION_2021) +fun `test extern without unsafe in edition 2021 is allowed`() = checkByCode(""" + extern "C" { + fn foo(); + } +""") + +fun `test safe mutable static in extern block is error`() = checkByCode(""" + unsafe extern "C" { + pub safe static mut FOO: i32; + } +""") + +fun `test safe immutable static in extern block is ok`() = checkByCode(""" + unsafe extern "C" { + pub safe static BAR: i32; + } +""") + +fun `test unsafe mutable static in extern block is ok`() = checkByCode(""" + unsafe extern "C" { + pub unsafe static mut FOO: i32; + } +""") +``` + +**Note:** Verify `@MockEdition` annotation exists, or adapt to existing edition mocking mechanism. + +**Effort:** 1 hour + +--- + +### 5.7 Run Edition Tests + +**Command:** +```bash +./gradlew :test --tests "org.rust.ide.annotator.RsErrorAnnotatorTest" +``` + +**Success Criteria:** +- [ ] Edition 2024 requires unsafe on extern blocks +- [ ] Pre-2024 editions allow extern without unsafe +- [ ] Inspection properly warns in pre-2024 editions +- [ ] Safe mutable statics are rejected +- [ ] Quick fix adds unsafe keyword correctly + +**Effort:** 30 minutes + fixes + +--- + +### Phase 5 Checkpoint + +**Verification Steps:** +1. Run error annotator tests +2. Test in RunIde with edition 2024 project +3. Test in RunIde with edition 2021 project +4. Verify quick fix works correctly + +**Success Criteria:** +- [ ] Edition 2024 enforces unsafe on extern blocks +- [ ] Pre-2024 editions show optional warning +- [ ] Quick fix adds unsafe keyword +- [ ] Safe mutable statics produce errors +- [ ] All tests pass + +**Blocking Issues:** +- Edition detection may vary by configuration - test with Cargo projects + +--- + +## Phase 6: IDE Features & Polish (COMP, FMT) + +**Estimated Effort:** 2-2.5 hours +**Prerequisites:** Phase 1-5 complete +**Branch:** Continue from Phase 5 + +--- + +### 6.1 Update Keyword Completion + +**File:** `src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributor.kt` + +**Analysis Required:** Find where `unsafe` is suggested for extern blocks + +**Add:** Suggest `safe` and `unsafe` for items inside extern blocks + +**Expected Location:** Completion provider for modifiers + +**Effort:** 1 hour + +--- + +### 6.2 Update Formatter (if needed) + +**Analysis Required:** Check if formatter needs updates for spacing around `safe` keyword + +**File:** `src/main/kotlin/org/rust/ide/formatter/impl/spacing.kt` + +**Expected:** Should handle automatically, but verify + +**Effort:** 30 minutes (investigation) + +--- + +### 6.3 Update Structure View (optional) + +**File:** Check structure view implementation + +**Consideration:** Should safe/unsafe qualifiers appear in structure view? + +**Effort:** 30 minutes (investigation + implementation if desired) + +--- + +### 6.4 Run IDE Feature Tests + +**Command:** +```bash +./gradlew :test --tests "*Completion*" +``` + +**Success Criteria:** +- [ ] Keyword completion includes safe/unsafe +- [ ] All IDE feature tests pass + +**Effort:** 30 minutes + fixes + +--- + +### Phase 6 Checkpoint + +**Verification Steps:** +1. Test code completion in RunIde +2. Verify formatter handles safe keyword +3. Check structure view + +**Success Criteria:** +- [ ] Completion suggests appropriate keywords +- [ ] Formatter handles safe keyword correctly +- [ ] All IDE feature tests pass + +**Blocking Issues:** None expected + +--- + +## Phase 7: Integration & Final Testing + +**Estimated Effort:** 4-6 hours +**Prerequisites:** All previous phases complete +**Branch:** Continue or create `feature/unsafe-extern-final` + +--- + +### 7.1 Run Full Test Suite + +**Command:** +```bash +./gradlew :test +``` + +**Expected:** 5000+ tests should pass + +**Effort:** 30 minutes (test execution time) + fixes + +--- + +### 7.2 Manual Testing in RunIde + +**Command:** +```bash +./gradlew :plugin:runIde +``` + +**Test Cases:** +1. Create edition 2024 project +2. Write extern block without unsafe - verify error +3. Apply quick fix - verify adds unsafe +4. Write safe foreign function - verify no unsafe required for calls +5. Write unsafe foreign function - verify unsafe required for calls +6. Test completion +7. Switch project to edition 2021 - verify no errors + +**Effort:** 2 hours + +--- + +### 7.3 Performance Testing + +**Considerations:** +- Stub version bump forces re-indexing +- Check indexing performance on large projects +- Verify no performance regression in name resolution + +**Effort:** 1 hour + +--- + +### 7.4 Update Documentation + +**Files to update:** +- `CHANGELOG.md` (if exists) +- `ARCHITECTURE.md` - document new PSI elements +- Update CLAUDE.md if needed + +**Effort:** 1 hour + +--- + +### 7.5 Verify Build Plugin + +**Command:** +```bash +./gradlew :plugin:buildPlugin +``` + +**Success Criteria:** +- [ ] Build succeeds +- [ ] Plugin ZIP created +- [ ] Can install in test IDE + +**Effort:** 15 minutes + +--- + +### Phase 7 Checkpoint + +**Verification Steps:** +1. All tests pass +2. Manual testing complete +3. No performance regressions +4. Documentation updated +5. Plugin builds successfully + +**Success Criteria:** +- [ ] All 5000+ tests pass +- [ ] Manual testing scenarios work +- [ ] No performance issues +- [ ] Documentation complete +- [ ] Plugin builds and installs + +**Blocking Issues:** Any test failures must be resolved + +--- + +## Final Checklist + +Before marking complete, verify: + +### Code Quality +- [ ] All new code follows project conventions +- [ ] Commit messages use appropriate tags (GRAM, PSI, STUB, etc.) +- [ ] No commented-out code +- [ ] No debug print statements + +### Testing +- [ ] All parser tests pass +- [ ] All stub tests pass +- [ ] All semantic tests pass +- [ ] All annotator tests pass +- [ ] All IDE feature tests pass +- [ ] Full test suite passes (5000+ tests) + +### Documentation +- [ ] CLAUDE.md updated if needed +- [ ] ARCHITECTURE.md updated if needed +- [ ] Code comments explain complex logic +- [ ] This MIGRATION_PLAN.md marked complete + +### Functionality +- [ ] Grammar accepts all unsafe extern syntax +- [ ] Stubs store isUnsafe flag correctly +- [ ] Edition 2024 enforces unsafe on extern blocks +- [ ] Safe functions don't require unsafe blocks for calls +- [ ] Unsafe functions require unsafe blocks for calls +- [ ] Quick fixes work correctly +- [ ] Completion suggests appropriate keywords + +### Compatibility +- [ ] Pre-2024 editions still work +- [ ] wasm_bindgen special case preserved +- [ ] No breaking changes to existing correct code +- [ ] Stub version bumped (forces re-index) + +--- + +## Risk Matrix + +| Risk | Likelihood | Impact | Mitigation | Status | +|------|-----------|--------|------------|--------| +| Stub format change breaks existing indexes | High | High | Bump stub version, communicate to users | ⚠️ Expected | +| Edition detection fails in some configs | Medium | Medium | Extensive testing with various project types | Monitor | +| Performance regression from additional checks | Low | Low | Profile before/after, optimize if needed | Minimal overhead | +| Breaking existing extern block code | Low | High | Comprehensive regression testing | Backward compatible | +| wasm_bindgen special case breaks | Low | High | Dedicated test coverage | Covered in tests | + +**Risks Mitigated by Architecture:** +- ✓ **Grammar conflicts** - Minimal: Simple flag additions to existing rules +- ✓ **API fragmentation** - None: No new PSI types, no breaking changes +- ✓ **Code duplication bugs** - None: Centralized implementation in existing types +- ✓ **Maintenance burden** - Low: Single update path for function/constant changes + +--- + +## Dependencies Between Phases + +``` +Phase 1 (Grammar) + ↓ +Phase 2 (PSI/Stubs) ← Requires generated parser from Phase 1 + ↓ +Phase 3 (Tests) ← Tests verify Phase 1 & 2 implementation + ↓ +Phase 4 (Semantics) ← Requires PSI from Phase 2 + ↓ +Phase 5 (Edition Validation) ← Requires semantics from Phase 4 + ↓ +Phase 6 (IDE Features) ← Requires all previous phases + ↓ +Phase 7 (Integration) ← Final validation of all phases +``` + +**Critical Path:** Phase 1 → Phase 2 → Phase 4 → Phase 5 +**Can be parallelized:** Some tests in Phase 3 can be written during Phase 1/2 + +--- + +## Effort Summary + +| Phase | Estimated Effort | Complexity | Notes | +|-------|------------------|------------|-------| +| Phase 1: Grammar Changes | 2.5-3.5 hours | Low | Grammar + parser util changes | +| Phase 2: PSI & Stub Changes | 4-5 hours | Medium-High (stub changes) | Adding flags to existing stubs | +| Phase 3: Parser & Stub Tests | 4-5 hours | Medium | Standard test patterns | +| Phase 4: Semantic Analysis | 6-8 hours | Medium-High | Update existing safety logic | +| Phase 5: Edition Validation | 3.5-5.5 hours | Medium | Edition-specific checks | +| Phase 6: IDE Features | 2-2.5 hours | Low-Medium | Completion | +| Phase 7: Integration & Testing | 4-6 hours | Medium | Full suite validation | +| **Total** | **26.5-36.5 hours** | **3.5-4.5 days** | | + +**Note:** Estimates assume familiarity with codebase and no major blockers. Effort reflects extending existing PSI infrastructure rather than creating new types. + +--- + +## Success Metrics + +### Functional Success +- All unsafe extern syntax variants parse correctly +- Edition 2024 projects require unsafe on extern blocks +- Safe foreign functions can be called without unsafe blocks +- Unsafe foreign functions require unsafe blocks +- Quick fixes and intentions work as expected + +### Quality Success +- Zero test regressions +- All new tests pass +- Code coverage maintained or improved +- No performance regressions + +### User Success +- Migration path clear for edition 2024 users +- Helpful error messages and quick fixes +- Documentation explains new syntax +- Existing code continues to work + +--- + +## Post-Implementation Tasks + +After completing all phases: + +1. **Code Review** + - Request review from maintainers + - Address feedback + +2. **Release Planning** + - Coordinate stub version bump with release + - Prepare release notes + - Test with beta users if available + +3. **User Communication** + - Announce feature in changelog + - Explain stub re-indexing requirement + - Provide migration guide for edition 2024 + +4. **Monitoring** + - Watch for bug reports + - Monitor performance metrics + - Gather user feedback + +--- + +## Notes & Open Questions + +1. **Stub Field Storage:** + - RsForeignModStub uses boolean for isUnsafe - this is acceptable and consistent with similar cases + - RsFunctionStub and RsConstantStub use bit flags - adding IS_SAFE_MASK fits existing pattern + - **Conclusion:** Current approach is consistent, no changes needed + +2. **Edition Detection:** The standard `isAtLeastEdition2024` property (from `RsElement.kt`) is used for edition checks, following the established pattern throughout the codebase. + - **Action:** Test in Phase 5 with various project types to ensure edition detection works correctly + +3. **Formatter:** Need to verify spacing around `safe` keyword matches existing formatter conventions. + - **Expected:** Should handle automatically like `unsafe`, `const`, `async` + - **Action:** Verify in Phase 6 + +4. **Structure View:** Should safe/unsafe qualifiers be displayed? Consider UX implications. + - **Defer:** Can be added in future enhancement if user feedback requests it + - **Not blocking:** Core functionality works without structure view changes + +--- + +## Version History + +| Version | Date | Changes | Author | +|---------|------|---------|--------| +| 1.0 | TBD | Initial migration plan created | - | +| 1.1 | 2025-10-31 | Finalized architectural approach based on 2016 patterns; added historical evidence and rationale; updated effort estimates (26-36h); added Architectural Rationale section | Claude Code | +| 1.2 | 2025-11-02 | Corrected Phase 1 to use proper Grammar-Kit contextual keyword pattern (`<>`); added RustParserUtil function step; clarified token remapping mechanism; updated effort to 26.5-36.5h | Claude Code | + +--- + +**Status:** Ready for Implementation (Planning Complete) +**Last Updated:** 2025-11-02 +**Assigned To:** TBD + +**Key Architectural Decisions:** +- ✅ Extend existing Function/Constant types (following 2016 architectural pattern) +- ✅ Use bit flags (SAFE_MASK for RsFunction, IS_SAFE_MASK/IS_UNSAFE_MASK for RsConstant) in existing stub infrastructure +- ✅ Leverage owner-based context detection (RsAbstractableOwner.Foreign) +- ✅ Implement `safe` as contextual keyword using Grammar-Kit meta-rules and token remapping +- ✅ Total effort: 26.5-36.5 hours From acfc645a654a38e42ab1de1dddc163b46c3e01f1 Mon Sep 17 00:00:00 2001 From: Zero King Date: Fri, 31 Oct 2025 00:24:58 +0800 Subject: [PATCH 04/10] GRAM: Add safe keyword support for unsafe extern blocks (Phase 1) Add grammar support for Rust 1.82's unsafe extern blocks feature (RFC 3484): - Add 'safe' keyword to lexer - Add SAFE token to parser - Update Function rule to support 'safe?' modifier - Update Constant rule to support '(safe | unsafe)?' qualifiers This enables parsing of: - unsafe extern "C" { ... } blocks - safe fn function_name() in extern blocks - unsafe fn function_name() in extern blocks - safe/unsafe static items in extern blocks Semantic validation (preventing both safe and unsafe on same item) will be added in Phase 4 following the plugin's established pattern of permissive parsing with semantic-level error checking. Part of unsafe extern blocks migration (Phase 1 of 7). Related to RFC 3484. --- src/main/grammars/RustLexer.flex | 1 + src/main/grammars/RustParser.bnf | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/grammars/RustLexer.flex b/src/main/grammars/RustLexer.flex index b4f1fbb66..13b00a649 100644 --- a/src/main/grammars/RustLexer.flex +++ b/src/main/grammars/RustLexer.flex @@ -220,6 +220,7 @@ EOL_DOC_LINE = {LINE_WS}*!(!("///".*)|("////".*)) "pub" { return PUB; } "ref" { return REF; } "return" { return RETURN; } + "safe" { return SAFE; } "Self" { return CSELF; } "self" { return SELF; } "static" { return STATIC; } diff --git a/src/main/grammars/RustParser.bnf b/src/main/grammars/RustParser.bnf index 8b6ac949b..4d5034a44 100644 --- a/src/main/grammars/RustParser.bnf +++ b/src/main/grammars/RustParser.bnf @@ -84,6 +84,7 @@ TRY = 'try_kw' GEN = 'gen_kw' RAW = 'raw_kw' + SAFE = 'safe' MACRO_KW = 'macro' CSELF = 'Self' @@ -533,7 +534,7 @@ RetType ::= '->' TypeReferenceNoImplicitTraitType { elementTypeFactory = "org.rust.lang.core.stubs.StubImplementationsKt.factory" } -upper Function ::= async? const? unsafe? ExternAbi? +upper Function ::= async? const? unsafe? safe? ExternAbi? fn identifier TypeParameterList? FnParameters @@ -650,7 +651,7 @@ PatField ::= PatFieldFull | box? PatBinding BindingMode ::= ref mut? | mut -upper Constant ::= (static mut? | const) (identifier | '_') TypeAscription? [ '=' AnyExpr ] ';' { +upper Constant ::= ((safe | unsafe) static mut? | static mut? | const) (identifier | '_') TypeAscription? [ '=' AnyExpr ] ';' { pin = 2 name = "" implements = [ "org.rust.lang.core.psi.ext.RsQualifiedNamedElement" From 08cbf5ccb6ec43d7e64bee722eef5f2cf62d56e2 Mon Sep 17 00:00:00 2001 From: Zero King Date: Fri, 31 Oct 2025 21:53:49 +0800 Subject: [PATCH 05/10] STUB, PSI: Add stub support for safe/unsafe in extern blocks (Phase 2) - Bump stub version from 234 to 235 - Add isUnsafe field to RsForeignModStub with serialization - Add SAFE_MASK to RsFunctionStub for safe keyword tracking - Add IS_SAFE_MASK and IS_UNSAFE_MASK to RsConstantStub - Add PSI extensions: RsForeignModItem.isUnsafe, RsFunction.isSafe, RsConstant.isSafe/isUnsafe Part of RFC 3484 unsafe extern blocks implementation. --- .../org/rust/lang/core/psi/ext/RsConstant.kt | 4 ++++ .../lang/core/psi/ext/RsForeignModItem.kt | 3 +++ .../org/rust/lang/core/psi/ext/RsFunction.kt | 3 +++ .../lang/core/stubs/StubImplementations.kt | 20 +++++++++++++++++-- .../rust/lang/core/stubs/StubInterfaces.kt | 2 ++ 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt index 19e0ac687..074a1c7f4 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt @@ -28,6 +28,10 @@ val RsConstant.isMut: Boolean get() = greenStub?.isMut ?: (mut != null) val RsConstant.isConst: Boolean get() = greenStub?.isConst ?: (const != null) +val RsConstant.isSafe: Boolean get() = greenStub?.isSafe ?: (safe != null) + +val RsConstant.isUnsafe: Boolean get() = greenStub?.isUnsafe ?: (unsafe != null) + val RsConstant.kind: RsConstantKind get() = when { isMut -> RsConstantKind.MUT_STATIC isConst -> RsConstantKind.CONST diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt index 3dc5c9d25..942fdfb44 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt @@ -25,6 +25,9 @@ val RsForeignModItem.abi: String? } } +val RsForeignModItem.isUnsafe: Boolean + get() = greenStub?.isUnsafe ?: (unsafe != null) + abstract class RsForeignModItemImplMixin : RsStubbedElementImpl, RsForeignModItem { diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt index 24b8470f3..0f2cafad2 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt @@ -75,6 +75,9 @@ val RsFunction.isVariadic: Boolean return stub?.isVariadic ?: (valueParameterList?.variadic != null) } +val RsFunction.isSafe: Boolean + get() = greenStub?.isSafe ?: (safe != null) + val RsFunction.literalAbiName: String? get() { val stub = greenStub diff --git a/src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt b/src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt index 4d26e2d1b..5b2bd6c06 100644 --- a/src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt +++ b/src/main/kotlin/org/rust/lang/core/stubs/StubImplementations.kt @@ -43,6 +43,7 @@ import org.rust.lang.core.stubs.RsAttributeOwnerStub.CommonStubAttrFlags.MAY_HAV import org.rust.lang.core.stubs.RsAttributeOwnerStub.FileStubAttrFlags.MAY_HAVE_RECURSION_LIMIT import org.rust.lang.core.stubs.RsAttributeOwnerStub.FileStubAttrFlags.MAY_HAVE_STDLIB_ATTRIBUTES import org.rust.lang.core.stubs.RsAttributeOwnerStub.FunctionStubAttrFlags.MAY_BE_PROC_MACRO_DEF +import org.rust.lang.core.stubs.RsAttributeOwnerStub.ConstantStubAttrFlags import org.rust.lang.core.stubs.RsAttributeOwnerStub.ModStubAttrFlags.MAY_HAVE_MACRO_USE import org.rust.lang.core.stubs.RsAttributeOwnerStub.UseItemStubAttrFlags.MAY_HAVE_PRELUDE_IMPORT import org.rust.lang.core.stubs.RsEmptyStmtType.shouldCreateStub @@ -86,7 +87,7 @@ class RsFileStub( object Type : IStubFileElementType(RsLanguage) { // Bump this number if Stub structure changes - private const val STUB_VERSION = 234 + private const val STUB_VERSION = 235 override fun getStubVersion(): Int = RustParserDefinition.PARSER_VERSION + RS_BUILTIN_ATTRIBUTES_VERSION + STUB_VERSION @@ -893,6 +894,7 @@ class RsFunctionStub( val isUnsafe: Boolean get() = BitUtil.isSet(flags, UNSAFE_MASK) val isExtern: Boolean get() = BitUtil.isSet(flags, EXTERN_MASK) val isVariadic: Boolean get() = BitUtil.isSet(flags, VARIADIC_MASK) + val isSafe: Boolean get() = BitUtil.isSet(flags, SAFE_MASK) val isAsync: Boolean get() = BitUtil.isSet(flags, ASYNC_MASK) // Method resolve optimization: stub field access is much faster than PSI traversing @@ -946,6 +948,7 @@ class RsFunctionStub( flags = BitUtil.set(flags, UNSAFE_MASK, psi.isUnsafe) flags = BitUtil.set(flags, EXTERN_MASK, psi.isExtern) flags = BitUtil.set(flags, VARIADIC_MASK, psi.isVariadic) + flags = BitUtil.set(flags, SAFE_MASK, psi.isSafe) flags = BitUtil.set(flags, ASYNC_MASK, psi.isAsync) flags = BitUtil.set(flags, HAS_SELF_PARAMETER_MASK, psi.hasSelfParameters) @@ -980,6 +983,7 @@ class RsFunctionStub( nextBitMask() // second bit mask.countTrailingZeroBits() } + private val SAFE_MASK: Int = nextBitMask() } } @@ -996,6 +1000,10 @@ class RsConstantStub( get() = BitUtil.isSet(flags, IS_MUT_MASK) val isConst: Boolean get() = BitUtil.isSet(flags, IS_CONST_MASK) + val isSafe: Boolean + get() = BitUtil.isSet(flags, IS_SAFE_MASK) + val isUnsafe: Boolean + get() = BitUtil.isSet(flags, IS_UNSAFE_MASK) object Type : RsStubElementType("CONSTANT") { override fun deserialize(dataStream: StubInputStream, parentStub: StubElement<*>?) = @@ -1021,6 +1029,8 @@ class RsConstantStub( var flags = RsAttributeOwnerStub.extractFlags(psi) flags = BitUtil.set(flags, IS_MUT_MASK, psi.isMut) flags = BitUtil.set(flags, IS_CONST_MASK, psi.isConst) + flags = BitUtil.set(flags, IS_SAFE_MASK, psi.isSafe) + flags = BitUtil.set(flags, IS_UNSAFE_MASK, psi.isUnsafe) val procMacroInfo = RsAttrProcMacroOwnerStub.extractTextAndOffset(flags, psi) @@ -1030,9 +1040,11 @@ class RsConstantStub( override fun indexStub(stub: RsConstantStub, sink: IndexSink) = sink.indexConstant(stub) } - companion object : BitFlagsBuilder(CommonStubAttrFlags, BYTE) { + companion object : BitFlagsBuilder(ConstantStubAttrFlags, INT) { private val IS_MUT_MASK: Int = nextBitMask() private val IS_CONST_MASK: Int = nextBitMask() + private val IS_SAFE_MASK: Int = nextBitMask() + private val IS_UNSAFE_MASK: Int = nextBitMask() } } @@ -1082,6 +1094,7 @@ class RsForeignModStub( override val flags: Int, override val procMacroInfo: RsProcMacroStubInfo?, val abi: String?, + val isUnsafe: Boolean ) : RsAttrProcMacroOwnerStubBase(parent, elementType) { object Type : RsStubElementType("FOREIGN_MOD_ITEM") { @@ -1092,6 +1105,7 @@ class RsForeignModStub( dataStream.readUnsignedByte(), RsProcMacroStubInfo.deserialize(dataStream), abi = dataStream.readNameString(), + isUnsafe = dataStream.readBoolean() ) override fun serialize(stub: RsForeignModStub, dataStream: StubOutputStream) = @@ -1099,6 +1113,7 @@ class RsForeignModStub( writeByte(stub.flags) RsProcMacroStubInfo.serialize(stub.procMacroInfo, dataStream) writeName(stub.abi) + writeBoolean(stub.isUnsafe) } override fun createPsi(stub: RsForeignModStub) = @@ -1111,6 +1126,7 @@ class RsForeignModStub( parentStub, this, flags, procMacroInfo, abi = psi.abi, + isUnsafe = psi.isUnsafe ) } } diff --git a/src/main/kotlin/org/rust/lang/core/stubs/StubInterfaces.kt b/src/main/kotlin/org/rust/lang/core/stubs/StubInterfaces.kt index 150bd3381..3351d3ad2 100644 --- a/src/main/kotlin/org/rust/lang/core/stubs/StubInterfaces.kt +++ b/src/main/kotlin/org/rust/lang/core/stubs/StubInterfaces.kt @@ -73,6 +73,8 @@ interface RsAttributeOwnerStub : RsAttributeOwnerPsiOrStub { val MAY_BE_PROC_MACRO_DEF: Int = nextBitMask() } + object ConstantStubAttrFlags : BitFlagsBuilder(CommonStubAttrFlags, Limit.BYTE) + object UseItemStubAttrFlags : BitFlagsBuilder(CommonStubAttrFlags, Limit.BYTE) { val MAY_HAVE_PRELUDE_IMPORT: Int = nextBitMask() } From f6128b4d9bf00b2183465acdb3f7a1507fbe4db3 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sat, 1 Nov 2025 19:55:06 +0800 Subject: [PATCH 06/10] T: Add parser and stub tests for unsafe extern blocks (Phase 3) - Add parser test fixture unsafe_extern_blocks.rs covering RFC 3484 syntax - Add test method for unsafe extern blocks in RsCompleteParsingTestCase - Update extern_block.rs comments to reflect edition 2024 validity - Regenerate extern_block.txt with updated PSI structure - Add RsForeignModStubTest for isUnsafe flag on extern blocks - Add RsForeignItemTest for safe/unsafe qualifiers on foreign items Parser tests: 4/4 passing Stub tests: 10/11 passing (1 requires Phase 4 semantic analysis) Part of RFC 3484 unsafe extern blocks implementation. --- .../core/parser/RsCompleteParsingTestCase.kt | 1 + .../rust/lang/core/psi/RsForeignItemTest.kt | 81 ++++ .../lang/core/stubs/RsForeignModStubTest.kt | 97 +++++ .../parser/fixtures/complete/extern_block.rs | 4 +- .../parser/fixtures/complete/extern_block.txt | 4 +- .../fixtures/complete/unsafe_extern_blocks.rs | 45 +++ .../complete/unsafe_extern_blocks.txt | 366 ++++++++++++++++++ 7 files changed, 594 insertions(+), 4 deletions(-) create mode 100644 src/test/kotlin/org/rust/lang/core/psi/RsForeignItemTest.kt create mode 100644 src/test/kotlin/org/rust/lang/core/stubs/RsForeignModStubTest.kt create mode 100644 src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.rs create mode 100644 src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt diff --git a/src/test/kotlin/org/rust/lang/core/parser/RsCompleteParsingTestCase.kt b/src/test/kotlin/org/rust/lang/core/parser/RsCompleteParsingTestCase.kt index fd66df244..d5cc7191a 100644 --- a/src/test/kotlin/org/rust/lang/core/parser/RsCompleteParsingTestCase.kt +++ b/src/test/kotlin/org/rust/lang/core/parser/RsCompleteParsingTestCase.kt @@ -27,6 +27,7 @@ class RsCompleteParsingTestCase : RsParsingTestCaseBase("complete") { fun `test extern crates`() = doTest(true) fun `test extern fns`() = doTest(true) fun `test extern block`() = doTest(true) + fun `test unsafe extern blocks`() = doTest(true) fun `test precedence`() = doTest(true) fun `test way too many parens`() = doTest(true) fun `test way too many braces`() = doTest(true) diff --git a/src/test/kotlin/org/rust/lang/core/psi/RsForeignItemTest.kt b/src/test/kotlin/org/rust/lang/core/psi/RsForeignItemTest.kt new file mode 100644 index 000000000..c273cd2b6 --- /dev/null +++ b/src/test/kotlin/org/rust/lang/core/psi/RsForeignItemTest.kt @@ -0,0 +1,81 @@ +/* + * Use of this source code is governed by the MIT license that can be + * found in the LICENSE file. + */ + +package org.rust.lang.core.psi + +import org.rust.RsTestBase +import org.rust.lang.core.psi.ext.descendantsOfType +import org.rust.lang.core.psi.ext.isActuallyUnsafe +import org.rust.lang.core.psi.ext.isSafe +import org.rust.lang.core.psi.ext.isUnsafe + +class RsForeignItemTest : RsTestBase() { + + fun `test safe foreign function`() { + InlineFile(""" + unsafe extern "C" { + pub safe fn safe_fn(); + } + """) + val fn = myFixture.file.descendantsOfType().single() + assertTrue("Function should be marked as safe", fn.isSafe) + assertFalse("Safe function should not be actually unsafe", fn.isActuallyUnsafe) + } + + fun `test unsafe foreign function`() { + InlineFile(""" + unsafe extern "C" { + pub unsafe fn unsafe_fn(); + } + """) + val fn = myFixture.file.descendantsOfType().single() + assertFalse("Function should not be marked as safe", fn.isSafe) + assertTrue("Unsafe function should be actually unsafe", fn.isActuallyUnsafe) + } + + fun `test default unsafe foreign function`() { + InlineFile(""" + unsafe extern "C" { + pub fn default_fn(); + } + """) + val fn = myFixture.file.descendantsOfType().single() + assertFalse("Function should not be marked as safe", fn.isSafe) + assertTrue("Default foreign function should be actually unsafe", fn.isActuallyUnsafe) + } + + fun `test safe foreign static`() { + InlineFile(""" + unsafe extern "C" { + pub safe static SAFE_STATIC: i32; + } + """) + val constant = myFixture.file.descendantsOfType().single() + assertTrue("Static should be marked as safe", constant.isSafe) + assertFalse("Static should not be marked as unsafe", constant.isUnsafe) + } + + fun `test unsafe foreign static`() { + InlineFile(""" + unsafe extern "C" { + pub unsafe static UNSAFE_STATIC: *const u8; + } + """) + val constant = myFixture.file.descendantsOfType().single() + assertFalse("Static should not be marked as safe", constant.isSafe) + assertTrue("Static should be marked as unsafe", constant.isUnsafe) + } + + fun `test default unsafe foreign static`() { + InlineFile(""" + unsafe extern "C" { + static DEFAULT_STATIC: *mut u8; + } + """) + val constant = myFixture.file.descendantsOfType().single() + assertFalse("Static should not be marked as safe", constant.isSafe) + assertFalse("Static should not be explicitly marked as unsafe", constant.isUnsafe) + } +} diff --git a/src/test/kotlin/org/rust/lang/core/stubs/RsForeignModStubTest.kt b/src/test/kotlin/org/rust/lang/core/stubs/RsForeignModStubTest.kt new file mode 100644 index 000000000..f87f9cbd7 --- /dev/null +++ b/src/test/kotlin/org/rust/lang/core/stubs/RsForeignModStubTest.kt @@ -0,0 +1,97 @@ +/* + * Use of this source code is governed by the MIT license that can be + * found in the LICENSE file. + */ + +package org.rust.lang.core.stubs + +import com.intellij.psi.impl.DebugUtil +import com.intellij.psi.stubs.StubTreeLoader +import org.intellij.lang.annotations.Language +import org.rust.RsTestBase +import org.rust.fileTreeFromText + +class RsForeignModStubTest : RsTestBase() { + + fun `test unsafe extern block stub`() = doStubTreeTest(""" + unsafe extern "C" { + fn foo(); + } + """, """ + RsFileStub + FOREIGN_MOD_ITEM:RsForeignModStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub + """) + + fun `test safe extern block stub (legacy)`() = doStubTreeTest(""" + extern "C" { + fn bar(); + } + """, """ + RsFileStub + FOREIGN_MOD_ITEM:RsForeignModStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub + """) + + fun `test unsafe extern without ABI`() = doStubTreeTest(""" + unsafe extern { + fn baz(); + } + """, """ + RsFileStub + FOREIGN_MOD_ITEM:RsForeignModStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub + """) + + fun `test extern block with multiple items`() = doStubTreeTest(""" + unsafe extern "C" { + fn foo(); + static BAR: i32; + fn baz(x: i32); + } + """, """ + RsFileStub + FOREIGN_MOD_ITEM:RsForeignModStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub + CONSTANT:RsConstantStub + PATH_TYPE:RsPathTypeStub + PATH:RsPathStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub + VALUE_PARAMETER:RsValueParameterStub + PATH_TYPE:RsPathTypeStub + PATH:RsPathStub + """) + + fun `test extern block with inner attributes`() = doStubTreeTest(""" + unsafe extern "C" { + #![allow(dead_code)] + fn foo(); + } + """, """ + RsFileStub + FOREIGN_MOD_ITEM:RsForeignModStub + INNER_ATTR:RsInnerAttrStub + META_ITEM:RsMetaItemStub + PATH:RsPathStub + META_ITEM_ARGS:RsMetaItemArgsStub + META_ITEM:RsMetaItemStub + PATH:RsPathStub + FUNCTION:RsFunctionStub + VALUE_PARAMETER_LIST:RsPlaceholderStub + """) + + private fun doStubTreeTest(@Language("Rust") code: String, expectedStubText: String) { + val fileName = "main.rs" + fileTreeFromText("//- $fileName\n$code").create() + val vFile = myFixture.findFileInTempDir(fileName) + val stubTree = StubTreeLoader.getInstance().readFromVFile(project, vFile) + ?: error("Stub tree is null") + val stubText = DebugUtil.stubTreeToString(stubTree.root) + assertEquals(expectedStubText.trimIndent() + "\n", stubText) + } +} diff --git a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.rs b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.rs index 6545f6f9a..1119d5aad 100644 --- a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.rs +++ b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.rs @@ -17,5 +17,5 @@ extern "R\u{0075}st" {} extern r"system" {} extern 'C' {} // semantically invalid pub extern {} // semantically invalid -unsafe extern {} // semantically invalid -pub unsafe extern {} // semantically invalid +unsafe extern {} // semantically valid in edition 2024 +pub unsafe extern {} // semantically valid in edition 2024 diff --git a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.txt b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.txt index b3490b2e2..5cc711c3f 100644 --- a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.txt +++ b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/extern_block.txt @@ -217,7 +217,7 @@ FILE PsiElement(})('}') PsiWhiteSpace(' ') RsForeignModItemImpl(FOREIGN_MOD_ITEM) - PsiComment()('// semantically invalid') + PsiComment()('// semantically valid in edition 2024') PsiWhiteSpace('\n') RsVisImpl(VIS) PsiElement(pub)('pub') @@ -230,4 +230,4 @@ FILE PsiElement({)('{') PsiElement(})('}') PsiWhiteSpace(' ') - PsiComment()('// semantically invalid') + PsiComment()('// semantically valid in edition 2024') \ No newline at end of file diff --git a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.rs b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.rs new file mode 100644 index 000000000..a7e545d80 --- /dev/null +++ b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.rs @@ -0,0 +1,45 @@ +// Edition 2024 syntax - extern blocks require unsafe +unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + pub unsafe fn strlen(p: *const u8) -> usize; + pub fn default_unsafe(ptr: *mut u8); // defaults to unsafe + + pub safe static SAFE_CONSTANT: i32; + pub unsafe static UNSAFE_PTR: *const u8; + static DEFAULT_UNSAFE: *mut u8; // defaults to unsafe + + pub type OpaqueType; +} + +// Multiple ABIs +unsafe extern { + safe fn no_abi_safe(); +} + +unsafe extern "system" { + unsafe fn system_unsafe(); +} + +unsafe extern "Rust" { + fn rust_abi_fn(); +} + +// Legacy syntax (pre-2024) - still valid without unsafe +extern "C" { + fn legacy_function(); + static LEGACY_STATIC: i32; +} + +// Nested in modules +mod ffi { + unsafe extern "C" { + pub safe fn nested_safe(); + } +} + +// With attributes +#[link(name = "mylib")] +unsafe extern "C" { + #[link_name = "actual_name"] + safe fn attributed_fn(); +} diff --git a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt new file mode 100644 index 000000000..36e66dd68 --- /dev/null +++ b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt @@ -0,0 +1,366 @@ +FILE + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiComment()('// Edition 2024 syntax - extern blocks require unsafe') + PsiWhiteSpace('\n') + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"C"') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(safe)('safe') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('sqrt') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + RsValueParameterImpl(VALUE_PARAMETER) + RsPatIdentImpl(PAT_IDENT) + RsPatBindingImpl(PAT_BINDING) + PsiElement(identifier)('x') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('f64') + PsiElement())(')') + PsiWhiteSpace(' ') + RsRetTypeImpl(RET_TYPE) + PsiElement(->)('->') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('f64') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('strlen') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + RsValueParameterImpl(VALUE_PARAMETER) + RsPatIdentImpl(PAT_IDENT) + RsPatBindingImpl(PAT_BINDING) + PsiElement(identifier)('p') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsRefLikeTypeImpl(REF_LIKE_TYPE) + PsiElement(*)('*') + PsiElement(const)('const') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('u8') + PsiElement())(')') + PsiWhiteSpace(' ') + RsRetTypeImpl(RET_TYPE) + PsiElement(->)('->') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('usize') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('default_unsafe') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + RsValueParameterImpl(VALUE_PARAMETER) + RsPatIdentImpl(PAT_IDENT) + RsPatBindingImpl(PAT_BINDING) + PsiElement(identifier)('ptr') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsRefLikeTypeImpl(REF_LIKE_TYPE) + PsiElement(*)('*') + PsiElement(mut)('mut') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('u8') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace(' ') + PsiComment()('// defaults to unsafe') + PsiWhiteSpace('\n\n ') + RsConstantImpl(CONSTANT) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(safe)('safe') + PsiWhiteSpace(' ') + PsiElement(static)('static') + PsiWhiteSpace(' ') + PsiElement(identifier)('SAFE_CONSTANT') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('i32') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + RsConstantImpl(CONSTANT) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + PsiElement(static)('static') + PsiWhiteSpace(' ') + PsiElement(identifier)('UNSAFE_PTR') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsRefLikeTypeImpl(REF_LIKE_TYPE) + PsiElement(*)('*') + PsiElement(const)('const') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('u8') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + RsConstantImpl(CONSTANT) + PsiElement(static)('static') + PsiWhiteSpace(' ') + PsiElement(identifier)('DEFAULT_UNSAFE') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsRefLikeTypeImpl(REF_LIKE_TYPE) + PsiElement(*)('*') + PsiElement(mut)('mut') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('u8') + PsiElement(;)(';') + PsiWhiteSpace(' ') + PsiComment()('// defaults to unsafe') + PsiWhiteSpace('\n\n ') + RsTypeAliasImpl(TYPE_ALIAS) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(type)('type') + PsiWhiteSpace(' ') + PsiElement(identifier)('OpaqueType') + PsiElement(;)(';') + PsiWhiteSpace('\n') + PsiElement(})('}') + PsiWhiteSpace('\n\n') + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiComment()('// Multiple ABIs') + PsiWhiteSpace('\n') + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + PsiElement(safe)('safe') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('no_abi_safe') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n') + PsiElement(})('}') + PsiWhiteSpace('\n\n') + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"system"') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('system_unsafe') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n') + PsiElement(})('}') + PsiWhiteSpace('\n\n') + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"Rust"') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('rust_abi_fn') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n') + PsiElement(})('}') + PsiWhiteSpace('\n\n') + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiComment()('// Legacy syntax (pre-2024) - still valid without unsafe') + PsiWhiteSpace('\n') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"C"') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('legacy_function') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + RsConstantImpl(CONSTANT) + PsiElement(static)('static') + PsiWhiteSpace(' ') + PsiElement(identifier)('LEGACY_STATIC') + PsiElement(:)(':') + PsiWhiteSpace(' ') + RsPathTypeImpl(PATH_TYPE) + RsPathImpl(PATH) + PsiElement(identifier)('i32') + PsiElement(;)(';') + PsiWhiteSpace('\n') + PsiElement(})('}') + PsiWhiteSpace('\n\n') + RsModItemImpl(MOD_ITEM) + PsiComment()('// Nested in modules') + PsiWhiteSpace('\n') + PsiElement(mod)('mod') + PsiWhiteSpace(' ') + PsiElement(identifier)('ffi') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"C"') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + RsVisImpl(VIS) + PsiElement(pub)('pub') + PsiWhiteSpace(' ') + PsiElement(safe)('safe') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('nested_safe') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n ') + PsiElement(})('}') + PsiWhiteSpace('\n') + PsiElement(})('}') + PsiWhiteSpace('\n\n') + RsForeignModItemImpl(FOREIGN_MOD_ITEM) + PsiComment()('// With attributes') + PsiWhiteSpace('\n') + RsOuterAttrImpl(OUTER_ATTR) + PsiElement(#)('#') + PsiElement([)('[') + RsMetaItemImpl(META_ITEM) + RsPathImpl(PATH) + PsiElement(identifier)('link') + RsMetaItemArgsImpl(META_ITEM_ARGS) + PsiElement(()('(') + RsMetaItemImpl(META_ITEM) + RsPathImpl(PATH) + PsiElement(identifier)('name') + PsiWhiteSpace(' ') + PsiElement(=)('=') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"mylib"') + PsiElement())(')') + PsiElement(])(']') + PsiWhiteSpace('\n') + PsiElement(unsafe)('unsafe') + PsiWhiteSpace(' ') + RsExternAbiImpl(EXTERN_ABI) + PsiElement(extern)('extern') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"C"') + PsiWhiteSpace(' ') + PsiElement({)('{') + PsiWhiteSpace('\n ') + RsFunctionImpl(FUNCTION) + RsOuterAttrImpl(OUTER_ATTR) + PsiElement(#)('#') + PsiElement([)('[') + RsMetaItemImpl(META_ITEM) + RsPathImpl(PATH) + PsiElement(identifier)('link_name') + PsiWhiteSpace(' ') + PsiElement(=)('=') + PsiWhiteSpace(' ') + RsLitExprImpl(LIT_EXPR) + PsiElement(STRING_LITERAL)('"actual_name"') + PsiElement(])(']') + PsiWhiteSpace('\n ') + PsiElement(safe)('safe') + PsiWhiteSpace(' ') + PsiElement(fn)('fn') + PsiWhiteSpace(' ') + PsiElement(identifier)('attributed_fn') + RsValueParameterListImpl(VALUE_PARAMETER_LIST) + PsiElement(()('(') + PsiElement())(')') + PsiElement(;)(';') + PsiWhiteSpace('\n') + PsiElement(})('}') \ No newline at end of file From 166c4d00c4477784a5c05e82d9b52acc6298cc27 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sat, 1 Nov 2025 20:40:03 +0800 Subject: [PATCH 07/10] ANN, TY, T: Implement semantic analysis for safe/unsafe foreign items (Phase 4) Updates isActuallyUnsafe to respect safe keyword in extern blocks: - Safe foreign functions can be called without unsafe blocks - Safe foreign statics can be accessed without unsafe blocks - Explicit unsafe modifier always makes items unsafe - Default (unmarked) items remain unsafe - Preserves wasm_bindgen and intrinsics special cases Changes: - RsFunction.isActuallyUnsafe: Check isSafe first, return false for safe items - RsUnsafeExpressionAnnotator: Skip safe foreign statics Tests: - Add RsForeignFunctionSafetyTest (6 tests for semantic analysis) - Add 6 annotator tests for safe/unsafe foreign items - All tests passing (64/64 across 3 test files) Part of RFC 3484 unsafe extern blocks implementation. --- .../annotator/RsUnsafeExpressionAnnotator.kt | 6 +- .../org/rust/lang/core/psi/ext/RsFunction.kt | 12 ++- .../RsUnsafeExpressionErrorAnnotatorTest.kt | 60 ++++++++++++ .../resolve/RsForeignFunctionSafetyTest.kt | 95 +++++++++++++++++++ 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/org/rust/lang/core/resolve/RsForeignFunctionSafetyTest.kt diff --git a/src/main/kotlin/org/rust/ide/annotator/RsUnsafeExpressionAnnotator.kt b/src/main/kotlin/org/rust/ide/annotator/RsUnsafeExpressionAnnotator.kt index 9dcf04fb8..8216ceff2 100644 --- a/src/main/kotlin/org/rust/ide/annotator/RsUnsafeExpressionAnnotator.kt +++ b/src/main/kotlin/org/rust/ide/annotator/RsUnsafeExpressionAnnotator.kt @@ -59,7 +59,11 @@ class RsUnsafeExpressionAnnotator : AnnotatorBase() { private fun annotateUnsafeStaticRef(expr: RsPathExpr, element: RsConstant, holder: RsAnnotationHolder) { val constantType = when { element.kind == RsConstantKind.MUT_STATIC -> RsBundle.message("inspection.message.mutable") - element.kind == RsConstantKind.STATIC && element.parent is RsForeignModItem -> RsBundle.message("inspection.message.extern") + element.kind == RsConstantKind.STATIC && element.parent is RsForeignModItem -> { + // Safe statics in foreign blocks don't require unsafe + if (element.isSafe) return + RsBundle.message("inspection.message.extern") + } else -> return } diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt index 0f2cafad2..900ab0d11 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt @@ -164,16 +164,24 @@ val RsFunction.declaration: String * A function is unsafe if defined with `unsafe` modifier or if defined inside a certain `extern` * block. But [RsFunction.isUnsafe] takes into account only `unsafe` modifier. [isActuallyUnsafe] * takes into account both cases. + * + * For functions in unsafe extern blocks, items marked with `safe` are safe to call. + * Items without explicit marker default to unsafe. */ val RsFunction.isActuallyUnsafe: Boolean get() { + // Explicit unsafe modifier always makes it unsafe if (isUnsafe) return true + val context = context return if (context is RsForeignModItem) { - // functions inside `extern` block are unsafe in most cases + // Explicit safe marker makes it safe + if (isSafe) return false + + // Special cases that override default unsafe behavior when { // #[wasm_bindgen] is a procedural macro that removes the following - // extern block, so all function inside it become safe. + // extern block, so all functions inside it become safe. // See https://github.com/rustwasm/wasm-bindgen context.queryAttributes.hasAttribute("wasm_bindgen") -> false // Some Rust intrinsics are safe. This info is hardcoded in compiler diff --git a/src/test/kotlin/org/rust/ide/annotator/RsUnsafeExpressionErrorAnnotatorTest.kt b/src/test/kotlin/org/rust/ide/annotator/RsUnsafeExpressionErrorAnnotatorTest.kt index 903ea0008..2b0c00df8 100644 --- a/src/test/kotlin/org/rust/ide/annotator/RsUnsafeExpressionErrorAnnotatorTest.kt +++ b/src/test/kotlin/org/rust/ide/annotator/RsUnsafeExpressionErrorAnnotatorTest.kt @@ -136,6 +136,66 @@ class RsUnsafeExpressionErrorAnnotatorTest : RsAnnotatorTestBase(RsUnsafeExpress } """) + fun `test safe foreign function call without unsafe block`() = checkErrors(""" + unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + } + + fn main() { + sqrt(4.0); // OK - no error + } + """) + + fun `test unsafe foreign function call requires unsafe block`() = checkErrors(""" + unsafe extern "C" { + pub unsafe fn dangerous(); + } + + fn main() { + dangerous(); + } + """) + + fun `test default foreign function call requires unsafe block`() = checkErrors(""" + unsafe extern "C" { + pub fn implicit_unsafe(); + } + + fn main() { + implicit_unsafe(); + } + """) + + fun `test safe foreign function call in unsafe block`() = checkErrors(""" + unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + } + + fn main() { + unsafe { sqrt(4.0); } // OK - safe functions don't need unsafe, but allowed + } + """) + + fun `test safe foreign static access without unsafe block`() = checkErrors(""" + unsafe extern "C" { + pub safe static SAFE_CONSTANT: i32; + } + + fn main() { + let x = SAFE_CONSTANT; // OK - no error + } + """) + + fun `test unsafe foreign static access requires unsafe block`() = checkErrors(""" + unsafe extern "C" { + pub unsafe static UNSAFE_PTR: *const u8; + } + + fn main() { + let x = UNSAFE_PTR; + } + """) + fun `test access union field outside unsafe block`() = checkErrors(""" union Foo { x: u32, diff --git a/src/test/kotlin/org/rust/lang/core/resolve/RsForeignFunctionSafetyTest.kt b/src/test/kotlin/org/rust/lang/core/resolve/RsForeignFunctionSafetyTest.kt new file mode 100644 index 000000000..15a7648fa --- /dev/null +++ b/src/test/kotlin/org/rust/lang/core/resolve/RsForeignFunctionSafetyTest.kt @@ -0,0 +1,95 @@ +/* + * Use of this source code is governed by the MIT license that can be + * found in the LICENSE file. + */ + +package org.rust.lang.core.resolve + +import org.rust.RsTestBase +import org.rust.lang.core.psi.RsFunction +import org.rust.lang.core.psi.ext.descendantsOfType +import org.rust.lang.core.psi.ext.isActuallyUnsafe + +class RsForeignFunctionSafetyTest : RsTestBase() { + + fun `test safe foreign function is safe`() { + InlineFile(""" + unsafe extern "C" { + pub safe fn sqrt(x: f64) -> f64; + } + + fn main() { + sqrt(4.0); // Should not require unsafe block + } + """) + val fns = myFixture.file.descendantsOfType() + val sqrtFn = fns.single { it.name == "sqrt" } + assertFalse("Safe foreign function should not be actually unsafe", sqrtFn.isActuallyUnsafe) + } + + fun `test unsafe foreign function is unsafe`() { + InlineFile(""" + unsafe extern "C" { + pub unsafe fn dangerous(); + } + + fn main() { + dangerous(); // Should require unsafe block + } + """) + val fns = myFixture.file.descendantsOfType() + val dangerousFn = fns.single { it.name == "dangerous" } + assertTrue("Unsafe foreign function should be actually unsafe", dangerousFn.isActuallyUnsafe) + } + + fun `test default foreign function is unsafe`() { + InlineFile(""" + unsafe extern "C" { + pub fn implicit_unsafe(); + } + + fn main() { + implicit_unsafe(); // Should require unsafe block + } + """) + val fns = myFixture.file.descendantsOfType() + val implicitUnsafeFn = fns.single { it.name == "implicit_unsafe" } + assertTrue("Default foreign function should be actually unsafe", implicitUnsafeFn.isActuallyUnsafe) + } + + fun `test wasm_bindgen special case still works`() { + InlineFile(""" + #[wasm_bindgen] + extern "C" { + pub fn console_log(); + } + + fn main() { + console_log(); // Should not require unsafe + } + """) + val fns = myFixture.file.descendantsOfType() + val consoleLogFn = fns.single { it.name == "console_log" } + assertFalse("wasm_bindgen function should not be actually unsafe", consoleLogFn.isActuallyUnsafe) + } + + fun `test explicit unsafe modifier always makes function unsafe`() { + InlineFile(""" + unsafe extern "C" { + pub unsafe fn explicit_unsafe(); + } + """) + val fn = myFixture.file.descendantsOfType().single() + assertTrue("Explicit unsafe modifier should make function actually unsafe", fn.isActuallyUnsafe) + } + + fun `test safe marker in legacy extern block`() { + InlineFile(""" + extern "C" { + pub safe fn safe_in_legacy(); + } + """) + val fn = myFixture.file.descendantsOfType().single() + assertFalse("Safe function in legacy extern block should not be actually unsafe", fn.isActuallyUnsafe) + } +} From ba4a3ee7a6152f060fba128d6da74dad0a249d4d Mon Sep 17 00:00:00 2001 From: Zero King Date: Sat, 1 Nov 2025 21:20:04 +0800 Subject: [PATCH 08/10] ANN, T: Add edition-based validation for unsafe extern blocks (Phase 5) Implement Edition 2024 validation requiring unsafe keyword on extern blocks and prevent safe mutable statics in foreign modules. - Add requiresUnsafeKeyword property to detect Edition 2024+ - Add checkMissingUnsafeOnExternBlock in RsErrorAnnotator - Add checkSafeMutableStatic validation for foreign statics - Create AddUnsafeToExternBlockFix quick fix - Add error messages to RsBundle.properties - Add edition-based tests (10 tests, all passing) --- .../rust/ide/annotator/RsErrorAnnotator.kt | 34 ++++++++++++++++ .../ide/fixes/AddUnsafeToExternBlockFix.kt | 30 ++++++++++++++ .../lang/core/psi/ext/RsForeignModItem.kt | 1 + .../resources/messages/RsBundle.properties | 2 + .../ide/annotator/RsErrorAnnotatorTest.kt | 39 +++++++++++++++++++ 5 files changed, 106 insertions(+) create mode 100644 src/main/kotlin/org/rust/ide/fixes/AddUnsafeToExternBlockFix.kt diff --git a/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt b/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt index ef1818e79..d50ac9c54 100644 --- a/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt +++ b/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt @@ -104,6 +104,7 @@ class RsErrorAnnotator : AnnotatorBase(), HighlightRangeExtension { override fun visitEnumItem(o: RsEnumItem) = checkEnumItem(rsHolder, o) override fun visitEnumVariant(o: RsEnumVariant) = checkEnumVariant(rsHolder, o) override fun visitExternAbi(o: RsExternAbi) = checkExternAbi(rsHolder, o) + override fun visitForeignModItem(o: RsForeignModItem) = checkForeignModItem(rsHolder, o) override fun visitFunction(o: RsFunction) = checkFunction(rsHolder, o) override fun visitImplItem(o: RsImplItem) = checkImpl(rsHolder, o) override fun visitLetDecl(o: RsLetDecl) = checkLetDecl(rsHolder, o) @@ -1242,6 +1243,39 @@ class RsErrorAnnotator : AnnotatorBase(), HighlightRangeExtension { private fun checkConstant(holder: RsAnnotationHolder, element: RsConstant) { collectDiagnostics(holder, element) checkDuplicates(holder, element) + checkSafeMutableStatic(holder, element) + } + + private fun checkForeignModItem(holder: RsAnnotationHolder, element: RsForeignModItem) { + checkMissingUnsafeOnExternBlock(holder, element) + } + + private fun checkMissingUnsafeOnExternBlock(holder: RsAnnotationHolder, element: RsForeignModItem) { + // Only check for edition 2024+ + if (!element.isAtLeastEdition2024) return + + if (!element.isUnsafe) { + val externKeyword = element.externAbi.extern + holder.createErrorAnnotation( + externKeyword, + RsBundle.message("inspection.message.extern.blocks.must.be.unsafe.in.edition.2024"), + AddUnsafeToExternBlockFix(element) + ) + } + } + + private fun checkSafeMutableStatic(holder: RsAnnotationHolder, element: RsConstant) { + // Only check statics inside extern blocks + val foreignMod = element.context as? RsForeignModItem ?: return + + // Mutable statics cannot be marked safe + if (element.isMut && element.isSafe) { + val safeKeyword = element.safe ?: return + holder.createErrorAnnotation( + safeKeyword, + RsBundle.message("inspection.message.mutable.static.items.in.extern.blocks.cannot.be.marked.safe") + ) + } } private fun checkFunction(holder: RsAnnotationHolder, fn: RsFunction) { diff --git a/src/main/kotlin/org/rust/ide/fixes/AddUnsafeToExternBlockFix.kt b/src/main/kotlin/org/rust/ide/fixes/AddUnsafeToExternBlockFix.kt new file mode 100644 index 000000000..d1e1e9b73 --- /dev/null +++ b/src/main/kotlin/org/rust/ide/fixes/AddUnsafeToExternBlockFix.kt @@ -0,0 +1,30 @@ +/* + * Use of this source code is governed by the MIT license that can be + * found in the LICENSE file. + */ + +package org.rust.ide.fixes + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import org.rust.RsBundle +import org.rust.lang.core.psi.RsForeignModItem +import org.rust.lang.core.psi.RsPsiFactory + +/** + * Quick fix to add `unsafe` keyword to extern blocks (required in Edition 2024). + */ +class AddUnsafeToExternBlockFix(element: RsForeignModItem) : RsQuickFixBase(element) { + + override fun getFamilyName() = text + override fun getText() = RsBundle.message("intention.name.add.unsafe.to", "extern block") + + override fun invoke(project: Project, editor: Editor?, element: RsForeignModItem) { + val externKeyword = element.externAbi.extern + val unsafeKeyword = RsPsiFactory(project).createUnsafeKeyword() + + element.addBefore(unsafeKeyword, externKeyword) + element.addAfter(RsPsiFactory(project).createWhitespace(" "), unsafeKeyword) + } +} diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt index 942fdfb44..bed527f0f 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsForeignModItem.kt @@ -8,6 +8,7 @@ package org.rust.lang.core.psi.ext import com.intellij.lang.ASTNode import com.intellij.psi.PsiElement import com.intellij.psi.stubs.IStubElementType +import org.rust.cargo.project.workspace.CargoWorkspace import org.rust.lang.core.macros.RsExpandedElement import org.rust.lang.core.psi.RsForeignModItem import org.rust.lang.core.stubs.RsForeignModStub diff --git a/src/main/resources/messages/RsBundle.properties b/src/main/resources/messages/RsBundle.properties index 92092d007..f071ab33c 100644 --- a/src/main/resources/messages/RsBundle.properties +++ b/src/main/resources/messages/RsBundle.properties @@ -1482,3 +1482,5 @@ month.number.01.12.zero.padded.to.2.digits.07=(07) Month number (01-12), zero-pa the.proleptic.gregorian.year.modulo.100.zero.padded.to.2.digits.01=(01) The proleptic Gregorian year modulo 100, zero-padded to 2 digits the.proleptic.gregorian.year.divided.by.100.zero.padded.to.2.digits.20=(20) The proleptic Gregorian year divided by 100, zero-padded to 2 digits the.full.proleptic.gregorian.year.zero.padded.to.4.digits.2021=(2021) The full proleptic Gregorian year, zero-padded to 4 digits +inspection.message.extern.blocks.must.be.unsafe.in.edition.2024=Extern blocks must be unsafe in Edition 2024 +inspection.message.mutable.static.items.in.extern.blocks.cannot.be.marked.safe=Mutable static items in extern blocks cannot be marked safe diff --git a/src/test/kotlin/org/rust/ide/annotator/RsErrorAnnotatorTest.kt b/src/test/kotlin/org/rust/ide/annotator/RsErrorAnnotatorTest.kt index 6a2d30286..a63a287a2 100644 --- a/src/test/kotlin/org/rust/ide/annotator/RsErrorAnnotatorTest.kt +++ b/src/test/kotlin/org/rust/ide/annotator/RsErrorAnnotatorTest.kt @@ -4252,4 +4252,43 @@ class RsErrorAnnotatorTest : RsAnnotatorTestBase(RsErrorAnnotator::class) { cr#"foo"#; } """) + + @MockEdition(Edition.EDITION_2024) + fun `test missing unsafe on extern in edition 2024`() = checkErrors(""" + extern "C" { + fn foo(); + } + """) + + @MockEdition(Edition.EDITION_2024) + fun `test unsafe extern in edition 2024`() = checkErrors(""" + unsafe extern "C" { + fn foo(); + } + """) + + @MockEdition(Edition.EDITION_2021) + fun `test extern without unsafe in edition 2021 is allowed`() = checkErrors(""" + extern "C" { + fn foo(); + } + """) + + fun `test safe mutable static in extern block is error`() = checkErrors(""" + unsafe extern "C" { + pub safe static mut FOO: i32; + } + """) + + fun `test safe immutable static in extern block is ok`() = checkErrors(""" + unsafe extern "C" { + pub safe static BAR: i32; + } + """) + + fun `test unsafe mutable static in extern block is ok`() = checkErrors(""" + unsafe extern "C" { + pub unsafe static mut FOO: i32; + } + """) } From f6dee9046bcccb9f0bba418ef52059d1391cf2bb Mon Sep 17 00:00:00 2001 From: Zero King Date: Sun, 2 Nov 2025 00:57:36 +0800 Subject: [PATCH 09/10] COMP, PSI, FMT, T: Add IDE features for unsafe extern blocks (Phase 6) Implements Phase 6 of the unsafe extern blocks migration plan, adding: - Keyword completion for safe/unsafe in extern blocks - Added SAFE to RS_KEYWORDS token set for proper formatter spacing This phase adds developer-facing IDE features that improve the experience of working with safe/unsafe foreign functions in Edition 2024 code. --- .../kotlin/org/rust/lang/core/RsPsiPattern.kt | 9 +++ .../completion/RsCompletionContributor.kt | 2 + .../RsKeywordCompletionContributor.kt | 7 ++ .../completion/RsKeywordCompletionProvider.kt | 2 +- .../org/rust/lang/core/psi/RsTokenType.kt | 2 +- .../RsKeywordCompletionContributorTest.kt | 75 +++++++++++++++++++ .../completion/RsVisibilityCompletionTest.kt | 36 +++++++++ 7 files changed, 131 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/rust/lang/core/RsPsiPattern.kt b/src/main/kotlin/org/rust/lang/core/RsPsiPattern.kt index aa05749d8..9851eaa52 100644 --- a/src/main/kotlin/org/rust/lang/core/RsPsiPattern.kt +++ b/src/main/kotlin/org/rust/lang/core/RsPsiPattern.kt @@ -275,6 +275,15 @@ object RsPsiPattern { return baseInherentImplDeclarationPattern().and(identifierStatementBeginningPattern()) } + fun baseForeignModDeclarationPattern(): PsiElementPattern.Capture { + return psiElement().withParent( + or( + psiElement(), + psiElement().withParent(RsForeignModItem::class.java) + ) + ) + } + private inline fun onItem(): PsiElementPattern.Capture { return psiElement().withSuperParent(2, rootMetaItem(ownerPattern = psiElement())) } diff --git a/src/main/kotlin/org/rust/lang/core/completion/RsCompletionContributor.kt b/src/main/kotlin/org/rust/lang/core/completion/RsCompletionContributor.kt index f62fb5d84..96b5bde76 100644 --- a/src/main/kotlin/org/rust/lang/core/completion/RsCompletionContributor.kt +++ b/src/main/kotlin/org/rust/lang/core/completion/RsCompletionContributor.kt @@ -10,6 +10,7 @@ import com.intellij.codeInsight.completion.impl.CompletionSorterImpl import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementWeigher import org.rust.lang.core.RsPsiPattern +import org.rust.lang.core.RsPsiPattern.baseForeignModDeclarationPattern import org.rust.lang.core.RsPsiPattern.declarationPattern import org.rust.lang.core.RsPsiPattern.inherentImplDeclarationPattern import org.rust.lang.core.completion.lint.RsClippyLintCompletionProvider @@ -48,6 +49,7 @@ class RsCompletionContributor : CompletionContributor() { extend(CompletionType.BASIC, RsLambdaExprCompletionProvider) extend(CompletionType.BASIC, RsPsiPattern.fieldVisibility, RsVisibilityCompletionProvider()) extend(CompletionType.BASIC, declarationPattern() or inherentImplDeclarationPattern(), RsVisibilityCompletionProvider()) + extend(CompletionType.BASIC, baseForeignModDeclarationPattern(), RsVisibilityCompletionProvider()) extend(CompletionType.BASIC, RsReprCompletionProvider) extend(CompletionType.BASIC, RsCrateTypeAttrCompletionProvider) extend(CompletionType.BASIC, RsExternAbiCompletionProvider) diff --git a/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributor.kt b/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributor.kt index ce72ded53..270c7f7ab 100644 --- a/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributor.kt +++ b/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributor.kt @@ -26,6 +26,7 @@ import org.rust.ide.template.postfix.fillMatchArms import org.rust.ide.utils.template.newTemplateBuilder import org.rust.lang.core.* import org.rust.lang.core.RsPsiPattern.baseDeclarationPattern +import org.rust.lang.core.RsPsiPattern.baseForeignModDeclarationPattern import org.rust.lang.core.RsPsiPattern.baseInherentImplDeclarationPattern import org.rust.lang.core.RsPsiPattern.baseTraitOrImplDeclaration import org.rust.lang.core.RsPsiPattern.declarationPattern @@ -87,6 +88,8 @@ class RsKeywordCompletionContributor : CompletionContributor(), DumbAware { RsKeywordCompletionProvider("const", "fn", "type", "unsafe")) extend(CompletionType.BASIC, unsafeTraitOrImplDeclarationPattern(), RsKeywordCompletionProvider("fn")) + extend(CompletionType.BASIC, foreignModDeclarationPattern(), + RsKeywordCompletionProvider("fn", "static", "type", "safe", "unsafe")) extend(CompletionType.BASIC, asyncDeclarationPattern(), RsKeywordCompletionProvider("fn")) extend(CompletionType.BASIC, afterVisInherentImplDeclarationPattern(), @@ -341,6 +344,10 @@ class RsKeywordCompletionContributor : CompletionContributor(), DumbAware { return baseTraitOrImplDeclaration().and(statementBeginningPattern("unsafe")) } + private fun foreignModDeclarationPattern(): PsiElementPattern.Capture { + return baseForeignModDeclarationPattern().and(statementBeginningPattern()) + } + private fun afterVisInherentImplDeclarationPattern(): PsiElementPattern.Capture { return baseInherentImplDeclarationPattern().and(afterVis()) } diff --git a/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionProvider.kt b/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionProvider.kt index 9a058d99c..c3d9f955e 100644 --- a/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionProvider.kt +++ b/src/main/kotlin/org/rust/lang/core/completion/RsKeywordCompletionProvider.kt @@ -34,7 +34,7 @@ fun InsertionContext.addSuffix(suffix: String) { } private val ALWAYS_NEEDS_SPACE = setOf("as", "crate", "const", "async", "enum", "extern", "fn", "for", "impl", "let", "mod", "mut", - "static", "struct", "trait", "type", "union", "unsafe", "use", "where") + "static", "struct", "trait", "type", "union", "safe", "unsafe", "use", "where") private fun addInsertionHandler(keyword: String, builder: LookupElementBuilder, parameters: CompletionParameters): LookupElementBuilder { diff --git a/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt b/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt index 2ae3de812..a1c5db5ab 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt @@ -34,7 +34,7 @@ val RS_KEYWORDS = tokenSetOf( MATCH, MOD, MOVE, MUT, PUB, RAW, REF, RETURN, - SELF, STATIC, STRUCT, SUPER, + SAFE, SELF, STATIC, STRUCT, SUPER, TRAIT, TYPE_KW, UNION, UNSAFE, USE, WHERE, WHILE, diff --git a/src/test/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributorTest.kt b/src/test/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributorTest.kt index 83f3e4bd9..30c11c8a3 100644 --- a/src/test/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributorTest.kt +++ b/src/test/kotlin/org/rust/lang/core/completion/RsKeywordCompletionContributorTest.kt @@ -291,6 +291,80 @@ class RsKeywordCompletionContributorTest : RsCompletionTestBase() { extern fn /*caret*/ """) + fun `test keywords inside extern block`() = checkCompletion(EXTERN_BLOCK_KEYWORDS, """ + unsafe extern "C" { + /*caret*/ + fn foo(); + } + """, """ + unsafe extern "C" { + /*lookup*/ /*caret*/ + fn foo(); + } + """) + + fun `test keywords inside extern block after statement`() = checkCompletion(EXTERN_BLOCK_KEYWORDS, """ + unsafe extern "C" { + fn foo(); + /*caret*/ + } + """, """ + unsafe extern "C" { + fn foo(); + /*lookup*/ /*caret*/ + } + """) + + fun `test safe inside extern block`() = checkCompletion("safe", """ + unsafe extern "C" { + saf/*caret*/ + } + """, """ + unsafe extern "C" { + safe /*caret*/ + } + """) + + fun `test unsafe inside extern block`() = checkCompletion("unsafe", """ + unsafe extern "C" { + uns/*caret*/ + } + """, """ + unsafe extern "C" { + unsafe /*caret*/ + } + """) + + fun `test fn inside extern block`() = checkCompletion("fn", """ + unsafe extern "C" { + f/*caret*/ + } + """, """ + unsafe extern "C" { + fn /*caret*/ + } + """) + + fun `test static inside extern block`() = checkCompletion("static", """ + unsafe extern "C" { + sta/*caret*/ + } + """, """ + unsafe extern "C" { + static /*caret*/ + } + """) + + fun `test type inside extern block`() = checkCompletion("type", """ + unsafe extern "C" { + typ/*caret*/ + } + """, """ + unsafe extern "C" { + type /*caret*/ + } + """) + fun `test unsafe fn`() = checkCompletion("fn", """ unsafe f/*caret*/ """, """ @@ -1364,5 +1438,6 @@ class RsKeywordCompletionContributorTest : RsCompletionTestBase() { companion object { private val MEMBERS_KEYWORDS = listOf("fn", "type", "const", "unsafe") + private val EXTERN_BLOCK_KEYWORDS = listOf("fn", "static", "type", "safe", "unsafe") } } diff --git a/src/test/kotlin/org/rust/lang/core/completion/RsVisibilityCompletionTest.kt b/src/test/kotlin/org/rust/lang/core/completion/RsVisibilityCompletionTest.kt index 3bf40be2b..caa4b692d 100644 --- a/src/test/kotlin/org/rust/lang/core/completion/RsVisibilityCompletionTest.kt +++ b/src/test/kotlin/org/rust/lang/core/completion/RsVisibilityCompletionTest.kt @@ -149,6 +149,42 @@ class RsVisibilityCompletionTest : RsCompletionTestBase() { /*caret*/mod foo; """) + fun `test pub completion in extern block`() = checkContainsCompletion(DEFAULT_VISIBILITIES, """ + extern "C" { + /*caret*/ + } + """) + + fun `test pub completion before fn in extern block`() = checkCompletion("pub", """ + unsafe extern "C" { + /*caret*/fn foo(); + } + """, """ + unsafe extern "C" { + pub /*caret*/fn foo(); + } + """) + + fun `test pub completion before static in extern block`() = checkCompletion("pub", """ + unsafe extern "C" { + /*caret*/static FOO: i32; + } + """, """ + unsafe extern "C" { + pub /*caret*/static FOO: i32; + } + """) + + fun `test pub completion before safe fn in extern block`() = checkCompletion("pub", """ + unsafe extern "C" { + /*caret*/safe fn sqrt(x: f64) -> f64; + } + """, """ + unsafe extern "C" { + pub /*caret*/safe fn sqrt(x: f64) -> f64; + } + """) + companion object { private val DEFAULT_VISIBILITIES = listOf("pub", "pub(crate)", "pub(super)", "pub()") } From 31cbfec2d4bd4173f2422d86aced1f5aea0bc048 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sun, 2 Nov 2025 09:13:32 +0800 Subject: [PATCH 10/10] GRAM, PSI: Fix safe keyword to be contextual instead of hard keyword Phase 1 implementation incorrectly added 'safe' as a hard keyword in the lexer, which broke parsing of standard library code that uses 'safe' as a variable name (e.g., `Ok(safe) => Some(safe)` in intrinsic.rs). This commit fixes the implementation by making 'safe' a contextual keyword following the established pattern for async, dyn, union, etc: - Remove 'safe' from RustLexer.flex (no hard keyword in lexer) - Change SAFE token to 'safe_kw' in RustParser.bnf (contextual keyword) - Add safeKeyword() parser utility that remaps IDENTIFIER to SAFE in context - Add SAFE to RS_CONTEXTUAL_KEYWORDS token set - Update PSI extensions to detect SAFE via node.findChildByType() Now 'safe' works as a keyword in extern blocks while remaining usable as a regular identifier everywhere else. Standard library parsing test now passes. Note: The parser allows safe/unsafe qualifiers on functions and statics at the grammar level (for better error recovery), but these qualifiers are semantically only valid within extern blocks. This is not currently enforced. Fixes parsing regression introduced in Phase 1 (commit 6a5cff3d1). --- src/main/grammars/RustLexer.flex | 1 - src/main/grammars/RustParser.bnf | 3 ++- .../kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt | 3 ++- .../kotlin/org/rust/lang/core/parser/RustParserUtil.kt | 3 +++ src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt | 4 ++-- .../kotlin/org/rust/lang/core/psi/ext/RsConstant.kt | 3 ++- .../kotlin/org/rust/lang/core/psi/ext/RsFunction.kt | 2 +- .../parser/fixtures/complete/unsafe_extern_blocks.txt | 10 +++++----- .../org/rust/lang/core/parser/fixtures/partial/fn.txt | 2 +- 9 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/grammars/RustLexer.flex b/src/main/grammars/RustLexer.flex index 13b00a649..b4f1fbb66 100644 --- a/src/main/grammars/RustLexer.flex +++ b/src/main/grammars/RustLexer.flex @@ -220,7 +220,6 @@ EOL_DOC_LINE = {LINE_WS}*!(!("///".*)|("////".*)) "pub" { return PUB; } "ref" { return REF; } "return" { return RETURN; } - "safe" { return SAFE; } "Self" { return CSELF; } "self" { return SELF; } "static" { return STATIC; } diff --git a/src/main/grammars/RustParser.bnf b/src/main/grammars/RustParser.bnf index 4d5034a44..b01c017cd 100644 --- a/src/main/grammars/RustParser.bnf +++ b/src/main/grammars/RustParser.bnf @@ -84,7 +84,7 @@ TRY = 'try_kw' GEN = 'gen_kw' RAW = 'raw_kw' - SAFE = 'safe' + SAFE = 'safe_kw' MACRO_KW = 'macro' CSELF = 'Self' @@ -318,6 +318,7 @@ private asyncBlock ::= <> private try ::= <> private gen ::= <> private raw ::= <> +private safe ::= <> private AttrsAndVis ::= OuterAttr* Vis? private AttrsAndVis_first ::= '#' | crate | pub diff --git a/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt b/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt index d50ac9c54..000fa6be9 100644 --- a/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt +++ b/src/main/kotlin/org/rust/ide/annotator/RsErrorAnnotator.kt @@ -70,6 +70,7 @@ import org.rust.lang.core.macros.macroExpansionManager import org.rust.lang.core.macros.proc.ProcMacroApplicationService import org.rust.lang.core.psi.* import org.rust.lang.core.psi.RsElementTypes.IDENTIFIER +import org.rust.lang.core.psi.RsElementTypes.SAFE import org.rust.lang.core.psi.ext.* import org.rust.lang.core.resolve.* import org.rust.lang.core.resolve.ref.deepResolve @@ -1270,7 +1271,7 @@ class RsErrorAnnotator : AnnotatorBase(), HighlightRangeExtension { // Mutable statics cannot be marked safe if (element.isMut && element.isSafe) { - val safeKeyword = element.safe ?: return + val safeKeyword = element.node.findChildByType(SAFE)?.psi ?: return holder.createErrorAnnotation( safeKeyword, RsBundle.message("inspection.message.mutable.static.items.in.extern.blocks.cannot.be.marked.safe") diff --git a/src/main/kotlin/org/rust/lang/core/parser/RustParserUtil.kt b/src/main/kotlin/org/rust/lang/core/parser/RustParserUtil.kt index 11bc1f9c3..1de815968 100644 --- a/src/main/kotlin/org/rust/lang/core/parser/RustParserUtil.kt +++ b/src/main/kotlin/org/rust/lang/core/parser/RustParserUtil.kt @@ -698,6 +698,9 @@ object RustParserUtil : GeneratedParserUtilBase() { @JvmStatic fun rawKeyword(b: PsiBuilder, level: Int): Boolean = contextualKeywordWithRollback(b, "raw", RAW) + @JvmStatic + fun safeKeyword(b: PsiBuilder, level: Int): Boolean = contextualKeyword(b, "safe", SAFE) + @JvmStatic fun parseSecondPlusInIncrement(b: PsiBuilder, level: Int): Boolean = noWhiteSpaceBefore(b, PLUS) diff --git a/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt b/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt index a1c5db5ab..8f942f63c 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/RsTokenType.kt @@ -34,7 +34,7 @@ val RS_KEYWORDS = tokenSetOf( MATCH, MOD, MOVE, MUT, PUB, RAW, REF, RETURN, - SAFE, SELF, STATIC, STRUCT, SUPER, + SELF, STATIC, STRUCT, SUPER, TRAIT, TYPE_KW, UNION, UNSAFE, USE, WHERE, WHILE, @@ -91,7 +91,7 @@ val RS_LITERALS = tokenSetOf( CHAR_LITERAL, BYTE_LITERAL, INTEGER_LITERAL, FLOAT_LITERAL, BOOL_LITERAL ) -val RS_CONTEXTUAL_KEYWORDS = tokenSetOf(DEFAULT, UNION, AUTO, DYN, RAW) +val RS_CONTEXTUAL_KEYWORDS = tokenSetOf(DEFAULT, UNION, AUTO, DYN, RAW, SAFE) val RS_EDITION_2018_KEYWORDS = tokenSetOf(ASYNC, TRY) val RS_EDITION_2024_KEYWORDS = tokenSetOf(GEN) diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt index 074a1c7f4..3b6d8b9eb 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsConstant.kt @@ -13,6 +13,7 @@ import org.rust.ide.icons.RsIcons import org.rust.lang.core.macros.RsExpandedElement import org.rust.lang.core.psi.RsConstant import org.rust.lang.core.psi.RsElementTypes.DEFAULT +import org.rust.lang.core.psi.RsElementTypes.SAFE import org.rust.lang.core.psi.RsPsiImplUtil import org.rust.lang.core.stubs.RsConstantStub import org.rust.lang.core.types.ty.Mutability @@ -28,7 +29,7 @@ val RsConstant.isMut: Boolean get() = greenStub?.isMut ?: (mut != null) val RsConstant.isConst: Boolean get() = greenStub?.isConst ?: (const != null) -val RsConstant.isSafe: Boolean get() = greenStub?.isSafe ?: (safe != null) +val RsConstant.isSafe: Boolean get() = greenStub?.isSafe ?: (node.findChildByType(SAFE) != null) val RsConstant.isUnsafe: Boolean get() = greenStub?.isUnsafe ?: (unsafe != null) diff --git a/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt b/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt index 900ab0d11..856b62f1b 100644 --- a/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt +++ b/src/main/kotlin/org/rust/lang/core/psi/ext/RsFunction.kt @@ -76,7 +76,7 @@ val RsFunction.isVariadic: Boolean } val RsFunction.isSafe: Boolean - get() = greenStub?.isSafe ?: (safe != null) + get() = greenStub?.isSafe ?: (node.findChildByType(RsElementTypes.SAFE) != null) val RsFunction.literalAbiName: String? get() { diff --git a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt index 36e66dd68..66e90e0f4 100644 --- a/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt +++ b/src/test/resources/org/rust/lang/core/parser/fixtures/complete/unsafe_extern_blocks.txt @@ -16,7 +16,7 @@ FILE RsVisImpl(VIS) PsiElement(pub)('pub') PsiWhiteSpace(' ') - PsiElement(safe)('safe') + PsiElement(safe_kw)('safe') PsiWhiteSpace(' ') PsiElement(fn)('fn') PsiWhiteSpace(' ') @@ -107,7 +107,7 @@ FILE RsVisImpl(VIS) PsiElement(pub)('pub') PsiWhiteSpace(' ') - PsiElement(safe)('safe') + PsiElement(safe_kw)('safe') PsiWhiteSpace(' ') PsiElement(static)('static') PsiWhiteSpace(' ') @@ -178,7 +178,7 @@ FILE PsiElement({)('{') PsiWhiteSpace('\n ') RsFunctionImpl(FUNCTION) - PsiElement(safe)('safe') + PsiElement(safe_kw)('safe') PsiWhiteSpace(' ') PsiElement(fn)('fn') PsiWhiteSpace(' ') @@ -293,7 +293,7 @@ FILE RsVisImpl(VIS) PsiElement(pub)('pub') PsiWhiteSpace(' ') - PsiElement(safe)('safe') + PsiElement(safe_kw)('safe') PsiWhiteSpace(' ') PsiElement(fn)('fn') PsiWhiteSpace(' ') @@ -353,7 +353,7 @@ FILE PsiElement(STRING_LITERAL)('"actual_name"') PsiElement(])(']') PsiWhiteSpace('\n ') - PsiElement(safe)('safe') + PsiElement(safe_kw)('safe') PsiWhiteSpace(' ') PsiElement(fn)('fn') PsiWhiteSpace(' ') diff --git a/src/test/resources/org/rust/lang/core/parser/fixtures/partial/fn.txt b/src/test/resources/org/rust/lang/core/parser/fixtures/partial/fn.txt index 07980e4db..c7cd3dd9e 100644 --- a/src/test/resources/org/rust/lang/core/parser/fixtures/partial/fn.txt +++ b/src/test/resources/org/rust/lang/core/parser/fixtures/partial/fn.txt @@ -106,7 +106,7 @@ FILE PsiElement(})('}') PsiWhiteSpace('\n\n') PsiElement(unsafe)('unsafe') - PsiErrorElement:extern, fn, impl, mod or trait expected, got 'const' + PsiErrorElement:extern, fn, impl, mod, static or trait expected, got 'const' PsiWhiteSpace(' ') RsFunctionImpl(FUNCTION)