Skip to content

Commit

Permalink
add annotations for unresolved references and unused declarations, im…
Browse files Browse the repository at this point in the history
…proved helpers for working with numeric literals.
  • Loading branch information
mike42 committed Feb 12, 2022
1 parent a1a561f commit 9522ceb
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 15 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 6502 Assembly Plugin for IntelliJ

This IntelliJ plugin provides basic support for 6502 assembly language. It is suitable for projects which use the `ca65` assembler to target the WDC 65c02, 65c816, and related microprocessors.
This is plugin for JetBrains IDE's, which provides basic support for 6502 assembly language. It is suitable for projects which use the `ca65` assembler to target the WDC 6502, 65C816, and related microprocessors.

![6502 Example in IntelliJ](screenshot/6502_intellij_example.png)

Expand All @@ -12,6 +12,8 @@ This IntelliJ plugin provides basic support for 6502 assembly language. It is su
- Refactor/rename a label and its usages
- Comment/uncomment blocks of code
- Code folding for scopes, procedures and macro definitions
- Completion suggestions for mnemonics and labels
- Warnings for undefined and unused symbols

## Installation

Expand All @@ -30,3 +32,4 @@ I'm aware of these other plugins, which are for different assemblers.
- [4ch1m/kick-assembler-acbg](https://github.com/4ch1m/kick-assembler-acbg) - Kick Assembler
- [67726e/IntelliJ-6502](https://github.com/67726e/IntelliJ-6502) - NESASM
- [matozoid/Intellij6502](https://github.com/matozoid/Intellij6502) - 64tass

8 changes: 3 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ apply plugin: 'org.jetbrains.grammarkit'
import org.jetbrains.grammarkit.tasks.*

group 'org.ca65'
version '1.5'
version '1.6'
sourceCompatibility = 11

repositories {
Expand Down Expand Up @@ -38,11 +38,9 @@ runPluginVerifier {
}

patchPluginXml {
changeNotes = """This change adds intention actions for converting numeric literals between hexadecimal, decimal and binary representations.
changeNotes = """This change adds warnings for unresolved references within the same assembly file, and also highlights unused declarations. The result is not correct for projects which include symbols from other files (as opposed to importing them), so this feature may be disabled per-project via an intention action.
It also adds a completion helper for assembly language mnemonics, an inspection to indicate when unsupported mnemonics are used, and a quick-fix for switching the CPU target to one which includes the unsupported mnemonic.
The 'Generic 6502 Project' template has also been improved."""
A new weak warning has been added to suggest padding hex and binary numbers to a whole byte, since eg. 7 digit binary numbers often indicate a problem."""
sinceBuild = '211.6693'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file
if (!canConvertToBinary(text)) {
return false;
}
setText(Asm6502Bundle.message("INTN.convert.to.bin", literal.getText()));
setText(Asm6502Bundle.message("INTN.convert.to.bin", literal.getText(), doConvertToBinary(literal.getText())));
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file
if (!canConvertToDecimal(text)) {
return false;
}
setText(Asm6502Bundle.message("INTN.convert.to.dec", literal.getText()));
setText(Asm6502Bundle.message("INTN.convert.to.dec", literal.getText(), doConvertToDecimal(literal.getText())));
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file
if (!canConvertToHex(text)) {
return false;
}
setText(Asm6502Bundle.message("INTN.convert.to.hex", literal.getText()));
setText(Asm6502Bundle.message("INTN.convert.to.hex", literal.getText(), doConvertToHex(literal.getText())));
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.ca65.action;

import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import org.ca65.Asm6502Bundle;
import org.ca65.config.AsmConfiguration;
import org.ca65.helpers.Cpu;
import org.jetbrains.annotations.NotNull;

public class DisableProjectReferenceCheckingIntentionAction implements IntentionAction {

public DisableProjectReferenceCheckingIntentionAction() {
}

@Override
public @IntentionName @NotNull String getText() {
return Asm6502Bundle.message("INTN.NAME.disable.reference.checking");
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return Asm6502Bundle.message("INTN.NAME.disable.reference.checking");
}

@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return true;
}

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
AsmConfiguration.getInstance(project).setReferenceCheckingEnabled(false);
DaemonCodeAnalyzer.getInstance(project).restart();
}

@Override
public boolean startInWriteAction() {
return false;
}
}
49 changes: 49 additions & 0 deletions src/main/java/org/ca65/action/PadNumberIntentionAction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.ca65.action;

import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.util.IncorrectOperationException;
import org.ca65.Asm6502Bundle;
import org.ca65.psi.AsmElementFactory;
import org.jetbrains.annotations.NotNull;

public class PadNumberIntentionAction implements IntentionAction {
private final PsiElement existingElement;
private final String suggestedReplacement;

public PadNumberIntentionAction(PsiElement existingElement, String suggestedReplacement) {
this.existingElement = existingElement;
this.suggestedReplacement = suggestedReplacement;
}

@Override
public @IntentionName @NotNull String getText() {
return Asm6502Bundle.message("INTN.pad.number", this.suggestedReplacement);
}

@Override
public @NotNull @IntentionFamilyName String getFamilyName() {
return Asm6502Bundle.message("INTN.NAME.pad.number");
}

@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
return true;
}

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
PsiElement newLiteral = AsmElementFactory.createNumericLiteral(project, suggestedReplacement);
existingElement.replace(newLiteral);
}

@Override
public boolean startInWriteAction() {
return true;
}
}
60 changes: 60 additions & 0 deletions src/main/java/org/ca65/annotator/AsmNumericLiteralAnnotator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.ca65.annotator;

import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.psi.PsiElement;
import org.apache.commons.lang.StringUtils;
import org.ca65.Asm6502Bundle;
import org.ca65.action.IntentionActionUtil;
import org.ca65.action.PadNumberIntentionAction;
import org.ca65.psi.AsmNumericLiteral;
import org.jetbrains.annotations.NotNull;

/**
* Provide weak warnings for hex and binary literals which are not whole bytes, eg. 7-digit binary numbers.
*/
public class AsmNumericLiteralAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if(!(element instanceof AsmNumericLiteral)) {
return;
}
// Separate checks for hex vs binary literals
String elementText = element.getText();
if(elementText.startsWith("$") &&elementText.length() > 1) {
annotateHex(element, holder, elementText.substring(1));
}
if(elementText.startsWith("%") &&elementText.length() > 1) {
annotateBinary(element, holder, elementText.substring(1));
}
}

private void annotateBinary(PsiElement element, AnnotationHolder holder, String binString) {
int len = binString.length();
int remainder = binString.length() % 8;
if(remainder == 0) {
return;
}
String suggestedReplacement = "%" + StringUtils.leftPad(binString, (len + 8) - remainder, "0");
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, Asm6502Bundle.message("INSPECT.binary.literal.length", element.getText()))
.range(element.getTextRange())
.highlightType(ProblemHighlightType.WEAK_WARNING)
.withFix(new PadNumberIntentionAction(element, suggestedReplacement))
.create();
}

private void annotateHex(PsiElement element, AnnotationHolder holder, String hexString) {
if(hexString.length() % 2 == 0) {
return;
}
String suggestedReplacement = "$0" + hexString;
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, Asm6502Bundle.message("INSPECT.hex.literal.length", element.getText()))
.range(element.getTextRange())
.highlightType(ProblemHighlightType.WEAK_WARNING)
.withFix(new PadNumberIntentionAction(element, suggestedReplacement))
.create();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.ca65.annotator;

import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import org.ca65.Asm6502Bundle;
import org.ca65.action.DisableProjectReferenceCheckingIntentionAction;
import org.ca65.config.AsmConfiguration;
import org.ca65.psi.AsmDotexpr;
import org.ca65.psi.impl.AsmIdentifierrImpl;
import org.jetbrains.annotations.NotNull;

/**
* Highlight references to symbols which are not defined in current file. Does not work with includes.
**/
public class AsmUnresolvedReferenceAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (!AsmConfiguration.getInstance(element.getProject()).isReferenceCheckingEnabled()) {
return; // Reference checking is disabled
}
if (!(element instanceof AsmIdentifierrImpl)) {
return;
}
if (isInMacroDef(element)) {
// Identifiers used in macros are not correct
return;
}
PsiReference reference = element.getReference();
if (reference != null && reference.resolve() != null) {
return; // definition exists
}
String elementName = ((AsmIdentifierrImpl) element).getName();
holder.newAnnotation(HighlightSeverity.ERROR, Asm6502Bundle.message("INSPECT.unresolved.reference", elementName))
.range(element.getTextRange())
.highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL)
.withFix(new DisableProjectReferenceCheckingIntentionAction())
.create();
}

private boolean isInMacroDef(PsiElement element) {
// Go up until element parent is file
while (element != null && !(element.getParent() instanceof PsiFile)) {
element = element.getParent();
}
if (element == null) {
return false;
}
// Make a guess for performance: most macros are quite short. Assume we are *not* in a macro if we don't
// find a '.macro' statement after some reasonable number of elements.
int iterMax = 250;
int i = 0;
// Not a nested structure (really should be..), so we scan backwards from here.
// if we hit a .endmacro or start of file, we are not in a macro.
// if we hit a .macro, we are.
while (element != null && i < iterMax) {
if (element instanceof AsmDotexpr) {
String elementText = element.getFirstChild() == null ? element.getText() : element.getFirstChild().getText();
if (".macro".equalsIgnoreCase(elementText) || ".mac".equalsIgnoreCase(elementText)) {
return true;
} else if (".endmacro".equalsIgnoreCase(elementText) || ".endmac".equalsIgnoreCase(elementText)) {
return false;
}
}
element = element.getPrevSibling();
i++;
}
return false;
}
}
62 changes: 62 additions & 0 deletions src/main/java/org/ca65/annotator/AsmUnusedReferenceAnnotator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.ca65.annotator;

import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.lang.annotation.AnnotationHolder;
import com.intellij.lang.annotation.Annotator;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNameIdentifierOwner;
import com.intellij.psi.PsiReference;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.util.Query;
import org.ca65.Asm6502Bundle;
import org.ca65.action.DisableProjectReferenceCheckingIntentionAction;
import org.ca65.config.AsmConfiguration;
import org.ca65.psi.AsmLabelDefinition;
import org.ca65.psi.impl.AsmIdentifierDefinitionImpl;
import org.ca65.psi.impl.AsmLabelDefinitionImpl;
import org.jetbrains.annotations.NotNull;

/**
* Highlight unused definitions in file. Does not catch symbols defined in includes, and is not efficient at all.
**/
public class AsmUnusedReferenceAnnotator implements Annotator {
@Override
public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
if (!AsmConfiguration.getInstance(element.getProject()).isReferenceCheckingEnabled()) {
return; // Reference checking is disabled
}
if (!(element instanceof AsmLabelDefinitionImpl || element instanceof AsmIdentifierDefinitionImpl)) {
return;
}
if (isReferenced((PsiNameIdentifierOwner) element)) {
return;
}
if(element instanceof AsmLabelDefinitionImpl && ((AsmLabelDefinition) element).getNameIdentifier() == null) {
return; // References to anonymous labels don't work
}
String elementName = ((PsiNameIdentifierOwner) element).getName();
holder.newAnnotation(HighlightSeverity.WEAK_WARNING, Asm6502Bundle.message("INSPECT.unused.reference", elementName))
.range(element.getTextRange())
.highlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL)
.withFix(new DisableProjectReferenceCheckingIntentionAction())
.create();
}

private boolean isReferenced(PsiNameIdentifierOwner element) {
final Query<PsiReference> refs = ReferencesSearch.search(element, GlobalSearchScope.fileScope(element.getContainingFile()), false);
if (element instanceof AsmLabelDefinitionImpl) {
// Simple case
PsiReference firstReference = refs.findFirst();
return firstReference != null;
}
// These turn up references to themselves, using text range to skip.
for (PsiReference ref : refs) {
if (!ref.getAbsoluteRange().equals(element.getTextRange())) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
public class UnsupportedMnemonicAnnotator implements Annotator {
@Override
public void annotate(@NotNull final PsiElement element, @NotNull AnnotationHolder holder) {
// Ensure the Psi Element is an expression
if (!(element instanceof AsmInstructionMnemonic)) {
return;
}
Expand Down
Loading

0 comments on commit 9522ceb

Please sign in to comment.