Add unresolved import and load inspection

This commit is contained in:
hgranthorner
2026-08-06 16:18:34 -04:00
parent 8fcd2cca05
commit c6b30e32d6
7 changed files with 118 additions and 12 deletions

View File

@@ -66,6 +66,7 @@ dev.hgh.jai.editor.JaiEditorSupportTest tests=7
dev.hgh.jai.editor.JaiFoldingBuilderTest tests=3
dev.hgh.jai.findusages.JaiFindUsagesTest tests=2
dev.hgh.jai.highlighting.* tests=8
dev.hgh.jai.inspection.JaiUnresolvedModuleInspectionTest tests=5
dev.hgh.jai.lexer.JaiCorpusLexerTest tests=2 <- the Tier 0 gate
dev.hgh.jai.lexer.JaiLexerTest tests=13
dev.hgh.jai.parser.JaiCorpusParserTest tests=1 <- the Tier 3 gate
@@ -78,7 +79,7 @@ dev.hgh.jai.structure.JaiStructureViewTest tests=3
dev.hgh.jai.formatter.JaiFormatterTest tests=4
dev.hgh.jai.settings.JaiProjectSettingsTest tests=3
dev.hgh.jai.settings.JaiConfiguredRootTest tests=6
-> total 99, failures+errors 0
-> total 104, failures+errors 0
```
The corpus gates report what they actually did; check both lines are still there:
@@ -134,6 +135,10 @@ Tier 3: parsed 714 files, 714 clean (100.0%), 0 with errors, 0 PsiErrorElements
block indentation, opaque `#asm` preservation, and a corpus-wide idempotence
gate. Headless formatter tests cover registration, representative formatting,
opaque/directive token preservation, and all 714 corpus files.
- **Phase 7a** — unresolved `#import`/`#load` module and file path inspection,
reusing the existing reference resolver. Headless tests cover unresolved
imports and loads, resolved project/local modules and files, and
`#import,string` exclusions.
### Lexer design facts worth knowing before touching it
@@ -202,14 +207,14 @@ Tier 3: parsed 714 files, 714 clean (100.0%), 0 with errors, 0 PsiErrorElements
### In progress — pick up here
1. **Phases 7+** — inspections, compiler integration, and other optional work
remain. `#asm` bodies are intentionally consumed opaquely, so nothing inside
them has PSI yet.
1. **Phases 7+** additional inspections, quick fixes, live templates, and
compiler integration remain. `#asm` bodies are intentionally consumed
opaquely, so nothing inside them has PSI yet.
### Next planned increment
- Decide whether to continue with inspections and compiler integration phases
(78).
- Expand Phase 7 with additional high-confidence inspections and quick fixes;
struct-field resolution and completion remain a separate known gap.
### Open questions for the user (unanswered)

View File

@@ -31,6 +31,7 @@
- PSI-aware formatter with operator and punctuation spacing, block indentation,
directive-flag handling, and preservation of opaque `#asm` bodies; formatting
is idempotent across all 714 corpus files.
- Inspection warnings for unresolved `#import` and `#load` module/file paths.
### Known gaps

View File

@@ -28,10 +28,11 @@ never from online material (see `AGENTS.md`).
- PSI-aware formatting for operator and punctuation spacing, block indentation,
directive flags, and opaque `#asm` bodies; formatting is idempotent across the
full local corpus
- Inspection warnings for unresolved `#import` and `#load` module/file paths
Remaining code-insight gaps include completion and symbol resolution for struct
fields and other type-qualified members. Inspections and compiler integration
are planned later.
fields and other type-qualified members. Additional inspections and compiler
integration are planned later.
See `docs/BUILD_PLAN.md` for the phase plan.
## Install it locally

View File

@@ -172,7 +172,7 @@ Each phase has a machine-checkable gate. Do not advance without a green gate.
| **5** | Completion (keywords, directives, module names, visible declarations, parameters, module aliases, incomplete-expression recovery), rename, find-usages | Tier 4 fixture tests — **done** (84-test suite) |
| **5a** | Configurable Jai module/import roots and indexed external search scope | Tier 4 settings, resolution, completion, navigation, and find-usages tests — **done** (93-test suite) |
| **6** | Formatter, code style settings | Formatter round-trip — **done**, formatting all 714 corpus files is idempotent |
| **7** | Inspections (e.g. `#must` misuse), quick fixes, live templates | Tier 4 + `verifyPlugin` |
| **7** | Inspections, quick fixes, live templates | Tier 4 + `verifyPlugin` — initial unresolved `#import`/`#load` path inspection implemented |
| **8** | Optional: run-configuration to invoke the `jai` compiler, parse its error output | Integration test against `~/.local/jai/bin` |
Phase 6's idempotence check (format twice, assert no change) is another
@@ -218,6 +218,6 @@ currently a committed milestone.
setup, works today), or install a system JDK via Homebrew? I'd default to the
wrapper.
Phases 06 are implemented and verified headlessly. The next decision is
whether to continue with the optional inspections and compiler integration work
in Phases 78.
Phases 06 are implemented and verified headlessly. Phase 7 has an initial
unresolved `#import`/`#load` path inspection; additional inspections and
compiler integration remain optional follow-up work.

View File

@@ -0,0 +1,45 @@
package dev.hgh.jai.inspection
import com.intellij.codeInspection.LocalInspectionTool
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiReferenceService
import dev.hgh.jai.lexer.JaiTokenTypes
import dev.hgh.jai.psi.JaiDirectiveExpr
import dev.hgh.jai.psi.JaiLiteralExpr
/** Reports file and module paths in #import/#load directives that do not resolve. */
class JaiUnresolvedModuleInspection : LocalInspectionTool() {
override fun getDisplayName(): String = "Unresolved Jai import or load"
override fun buildVisitor(
holder: ProblemsHolder,
isOnTheFly: Boolean,
): PsiElementVisitor =
object : PsiElementVisitor() {
override fun visitElement(element: PsiElement) {
val literal = element as? JaiLiteralExpr ?: return
val directive = literal.parent as? JaiDirectiveExpr ?: return
val directiveName = directive.node.findChildByType(JaiTokenTypes.DIRECTIVE)?.text ?: return
if (directiveName != "#import" && directiveName != "#load") return
val reference =
PsiReferenceService
.getService()
.getContributedReferences(literal)
.firstOrNull()
?: return
if (reference.resolve() != null) return
val path = literal.text.removeSurrounding("\"")
holder.registerProblem(
reference.element,
"Cannot resolve $directiveName path '$path'",
ProblemHighlightType.LIKE_UNKNOWN_SYMBOL,
reference.rangeInElement,
)
}
}
}

View File

@@ -49,6 +49,13 @@
<psi.referenceContributor
language="Jai"
implementation="dev.hgh.jai.reference.JaiReferenceContributor"/>
<localInspection
language="Jai"
displayName="Unresolved Jai import or load"
groupName="Jai"
enabledByDefault="true"
level="WARNING"
implementationClass="dev.hgh.jai.inspection.JaiUnresolvedModuleInspection"/>
<completion.contributor
language="Jai"
implementationClass="dev.hgh.jai.completion.JaiCompletionContributor"/>

View File

@@ -0,0 +1,47 @@
package dev.hgh.jai.inspection
import com.intellij.testFramework.fixtures.BasePlatformTestCase
class JaiUnresolvedModuleInspectionTest : BasePlatformTestCase() {
override fun setUp() {
super.setUp()
myFixture.enableInspections(JaiUnresolvedModuleInspection::class.java)
}
fun testUnresolvedLoadPathIsReported() {
myFixture.configureByText(
"main.jai",
"#load \"<warning descr=\"Cannot resolve #load path 'missing.jai'\">missing.jai</warning>\";",
)
myFixture.checkHighlighting()
}
fun testUnresolvedImportPathIsReported() {
myFixture.configureByText(
"main.jai",
"#import \"<warning descr=\"Cannot resolve #import path 'missing-module'\">missing-module</warning>\";",
)
myFixture.checkHighlighting()
}
fun testResolvedLoadPathIsNotReported() {
myFixture.addFileToProject("other.jai", "value := 1;")
myFixture.configureByText("main.jai", "#load \"other.jai\";")
myFixture.checkHighlighting()
}
fun testResolvedImportPathIsNotReported() {
myFixture.configureByText("main.jai", "#import \"Basic\";")
myFixture.checkHighlighting()
}
fun testImportStringIsNotReportedAsAnUnresolvedPath() {
myFixture.configureByText("main.jai", "#import,string \"inline code\";")
myFixture.checkHighlighting()
}
}