Repository: PlasmaFAIR/fortitude (language: python)

Issue:
Prefer `interface` to `external`
Explicit interface declarations are far less error prone than `EXTERNAL` declarations:

```f90
interface
  function f(foo, bar)
    implicit none
    integer, intent(in) :: foo
    real, intent(in):: bar
    
    ...

  end function f
end interface

! Alternatively...
external :: f ! No args specified

x = f(foo, bar)
```


Discussion / hints:

Fix patch:
diff --git a/fortitude/src/rules/mod.rs b/fortitude/src/rules/mod.rs
index f376f9e..c3546f8 100644
--- a/fortitude/src/rules/mod.rs
+++ b/fortitude/src/rules/mod.rs
@@ -95,6 +95,7 @@ pub fn code_to_rule(category: Category, code: &str) -> Option<(RuleGroup, Rule)>
         (Typing, "042") => (RuleGroup::Stable, Ast, typing::assumed_size::AssumedSizeCharacterIntent),
         (Typing, "043") => (RuleGroup::Stable, Ast, typing::assumed_size::DeprecatedAssumedSizeCharacter),
         (Typing, "051") => (RuleGroup::Stable, Ast, typing::init_decls::InitialisationInDeclaration),
+        (Typing, "061") => (RuleGroup::Preview, Ast, typing::external::ExternalStatement),
 
         (Obsolescent, "001") => (RuleGroup::Preview, Ast, obsolescent::statement_functions::StatementFunction),
         (Obsolescent, "011") => (RuleGroup::Preview, Ast, obsolescent::common_blocks::CommonBlock),
diff --git a/fortitude/src/rules/typing/external.rs b/fortitude/src/rules/typing/external.rs
new file mode 100644
index 0000000..f509747
--- /dev/null
+++ b/fortitude/src/rules/typing/external.rs
@@ -0,0 +1,55 @@
+use ruff_diagnostics::{Diagnostic, Violation};
+use ruff_macros::{derive_message_formats, violation};
+use ruff_source_file::SourceFile;
+use tree_sitter::Node;
+
+use crate::{ast::FortitudeNode, settings::Settings, AstRule, FromAstNode};
+
+/// ## What does it do?
+/// Checks for procedures declared with just `external`
+///
+/// ## Why is this bad?
+/// Compilers are unable to check external procedures without an explicit
+/// interface for errors such as wrong number or type of arguments.
+///
+/// If the procedure is in your project, put it in a module (see
+/// `external-function`), or write an explicit interface.
+#[violation]
+pub struct ExternalStatement {
+    name: String,
+}
+
+impl Violation for ExternalStatement {
+    #[derive_message_formats]
+    fn message(&self) -> String {
+        let Self { name } = self;
+        format!("'{name}' declared as `external`")
+    }
+
+    fn fix_title(&self) -> Option<String> {
+        Some(format!("Write an explicit interface"))
+    }
+}
+
+impl AstRule for ExternalStatement {
+    fn check(_settings: &Settings, node: &Node, source: &SourceFile) -> Option<Vec<Diagnostic>> {
+        if node
+            .child_with_name("type_qualifier")?
+            .to_text(source.source_text())?
+            .to_lowercase()
+            != "external"
+        {
+            return None;
+        }
+
+        let name = node
+            .child_by_field_name("declarator")?
+            .to_text(source.source_text())?
+            .to_string();
+        some_vec!(Diagnostic::from_node(Self { name }, node))
+    }
+
+    fn entrypoints() -> Vec<&'static str> {
+        vec!["variable_modification"]
+    }
+}
diff --git a/fortitude/src/rules/typing/mod.rs b/fortitude/src/rules/typing/mod.rs
index 104a5b4..99a2fd8 100644
--- a/fortitude/src/rules/typing/mod.rs
+++ b/fortitude/src/rules/typing/mod.rs
@@ -1,4 +1,5 @@
 pub mod assumed_size;
+pub mod external;
 pub mod implicit_typing;
 pub mod init_decls;
 pub mod intent;
@@ -30,6 +31,7 @@ mod tests {
     #[test_case(Rule::AssumedSizeCharacterIntent, Path::new("T042.f90"))]
     #[test_case(Rule::DeprecatedAssumedSizeCharacter, Path::new("T043.f90"))]
     #[test_case(Rule::InitialisationInDeclaration, Path::new("T051.f90"))]
+    #[test_case(Rule::ExternalStatement, Path::new("T061.f90"))]
     fn rules(rule_code: Rule, path: &Path) -> Result<()> {
         let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
         let diagnostics = test_path(


Test patch:
diff --git a/fortitude/resources/test/fixtures/typing/T061.f90 b/fortitude/resources/test/fixtures/typing/T061.f90
new file mode 100644
index 0000000..3772595
--- /dev/null
+++ b/fortitude/resources/test/fixtures/typing/T061.f90
@@ -0,0 +1,14 @@
+program test
+  implicit none (type, external)
+
+  interface
+    integer function f(foo, bar)
+      implicit none
+      integer, intent(in) :: foo
+      integer, intent(in):: bar
+    end function f
+  end interface
+
+  external :: g
+
+end program test
diff --git a/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__external-statement_T061.f90.snap b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__external-statement_T061.f90.snap
new file mode 100644
index 0000000..2ea1bb6
--- /dev/null
+++ b/fortitude/src/rules/typing/snapshots/fortitude__rules__typing__tests__external-statement_T061.f90.snap
@@ -0,0 +1,15 @@
+---
+source: fortitude/src/rules/typing/mod.rs
+expression: diagnostics
+snapshot_kind: text
+---
+./resources/test/fixtures/typing/T061.f90:12:3: T061 'g' declared as `external`
+   |
+10 |   end interface
+11 |
+12 |   external :: g
+   |   ^^^^^^^^^^^^^ T061
+13 |
+14 | end program test
+   |
+   = help: Write an explicit interface