Documentation and Editor Tooling

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. The tools on this page are not part of the language: they are community and vendor tools that read Fortran source. They were installed inside a Linux container (aarch64) with pip or apt, nothing on the host, and every output shown is real output. Versions: FORD 7.0.13, fortls 3.2.2, fprettify 0.3.7, findent 4.3.5, Fortitude 0.9.2 (package fortitude-lint), fpm 0.13.0, CMake 3.31.6, Meson 1.7.0 (the current Meson release is 1.12), and gfortran 16.2. Where tools are described as community tools, that is a statement about who maintains them, not about quality. The documentation-commented heatflow sources used below were also built and run with fpm run under -std=f2023 -Wall -Wextra -pedantic -fcheck=all; the output is identical to the undocumented project (steps=2000 residual= 1.38E-04, max temperature= 0.139), because comments do not change the program. LFortran, Intel ifx and NAG were not used here, so nothing below claims how they behave. The tools on this page work on source text and do not need any particular compiler, except that the compile_commands.json section shows what each build system writes.

For the IDEs and editors themselves, see IDEs & Tools. The page follows the way a project grows. First the comments that become documentation (FORD), then the editor-side tools that understand the project (the fortls language server), then the formatters and linters that keep the source consistent, then the build-system output that tools use to learn compile flags (compile_commands.json), and finally the places where the community meets. Code layout and naming conventions are in Best Practices and Style, builds in Fortran Package Manager (fpm) and Building with Make, CMake and Meson, compilers and warnings in Compilers and Compiler Options, and test and debug tools in Testing, Debugging and Profiling.

flowchart LR SRC["Fortran sources src/ app/ test/"] FORD["FORD HTML documentation"] FORTLS["fortls language server"] FMT["fprettify / findent formatters"] LINT["Fortitude / compiler warnings linters"] CC["compile_commands.json from fpm, CMake or Meson"] SRC --> FORD SRC --> FORTLS SRC --> FMT SRC --> LINT CC -.-> LINT FORTLS --> ED["Editor hover, definition, rename"] FORD --> WEB["Published site graphs, search"]

The standard has no documentation format

Fortran defines no documentation-comment syntax. A comment starts with ! in free form (S 6.3.2.3) and the processor ignores it, so every documentation convention is a convention of a tool. Doxygen can read Fortran, but the tool built for modern Fortran is FORD (FORtran Documenter), which understands modules, submodules, derived types, type-bound procedures, generic interfaces, use relationships and call trees, none of which a generic comment extractor sees. FORD is a Python program, installed with pip install FORD; it needs the Graphviz dot program for call graphs and the pcpp package for preprocessing (installed together with FORD).

FORD doc comments

FORD attaches a comment to the entity it sits next to. The comment markers are configurable; the defaults, read from the installed FORD 7.0.13 settings, are docmark = "!", predocmark = ">" and two alternative marks. In practice this gives two forms:

  • Before the entity: a comment whose second character is the predoc mark, !>, directly above a module, type, procedure or variable.

  • After the entity: a comment whose second character is the doc mark, !!, on the lines following the declaration, or on the same line after a declaration (the !! form documents arguments and components inline).

The text is Markdown (Python-Markdown with FORD extensions). FORD recognises the admonition tags @note, @warning, @bug, @todo and @history, and inline TeX inside ... or \( ... \) is rendered with MathJax in the browser. Below is the heatflow kernels module as it was documented for this page; the code is unchanged apart from the comments.

!> Stencil kernels shared by the solvers, the parallel variants and the C API.
!>
!> Both kernels update the interior points of a rectangular grid and report
!> the largest change as a residual:
!>
!> $$ u^{n+1}_{i,j} = u^n_{i,j} + \alpha\,\Delta t\,\nabla^2 u^n_{i,j} $$
!>
!> @note Boundary points are never touched; apply the boundary condition
!> with `grid_t%apply_boundary` before each sweep.
module heatflow_kernels
  use heatflow_kinds, only: dp, ip
  implicit none
  private
  public :: jacobi_sweep, ftcs_sweep

contains

  !> One Jacobi relaxation sweep of the Laplace equation (interior points).
  !>
  !> @warning `unew` must have the same shape as `u`.
  pure subroutine jacobi_sweep(u, unew, residual)
    real(dp), intent(in) :: u(:, :)        !! Current temperature field.
    real(dp), intent(inout) :: unew(:, :)  !! Updated field (interior points only).
    real(dp), intent(out) :: residual      !! Largest absolute change of any point.
    integer :: i, j
    residual = 0.0_dp
    do j = 2, size(u, 2) - 1
      do i = 2, size(u, 1) - 1
        unew(i, j) = 0.25_dp*(u(i - 1, j) + u(i + 1, j) + u(i, j - 1) + u(i, j + 1))
        residual = max(residual, abs(unew(i, j) - u(i, j)))
      end do
    end do
  end subroutine jacobi_sweep

  !> One explicit FTCS step of \(u_t = \alpha (u_{xx} + u_{yy})\) (interior points).
  !>
  !> The scheme is stable only for \( \alpha\,\Delta t\,(1/\Delta x^2 + 1/\Delta y^2) \le 1/2 \).
  pure subroutine ftcs_sweep(u, unew, alpha, dt, dx, dy, residual)
    real(dp), intent(in) :: u(:, :)        !! Current temperature field.
    real(dp), intent(inout) :: unew(:, :)  !! Updated field (interior points only).
    real(dp), intent(in) :: alpha          !! Thermal diffusivity.
    real(dp), intent(in) :: dt             !! Time step.
    real(dp), intent(in) :: dx, dy         !! Grid spacings.
    real(dp), intent(out) :: residual      !! Largest absolute change of any point.
    integer :: i, j
    residual = 0.0_dp
    do j = 2, size(u, 2) - 1
      do i = 2, size(u, 1) - 1
        unew(i, j) = u(i, j) + alpha*dt*( &
                     (u(i - 1, j) - 2.0_dp*u(i, j) + u(i + 1, j))/dx**2 + &
                     (u(i, j - 1) - 2.0_dp*u(i, j) + u(i, j + 1))/dy**2)
        residual = max(residual, abs(unew(i, j) - u(i, j)))
      end do
    end do
  end subroutine ftcs_sweep
end module heatflow_kernels

The kinds module and the grid type were documented in the same way (!> above the module, !> above type :: grid_t, and a trailing !! on each component and kind parameter):

!> Kind parameters shared by every heatflow module.
module heatflow_kinds
  use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
  implicit none
  private
  public :: sp, dp, ip, lp
  integer, parameter :: sp = real32 !! Single precision real kind.
  integer, parameter :: dp = real64 !! Double precision real kind used for temperatures.
  integer, parameter :: ip = int32  !! Default integer kind for grid sizes.
  integer, parameter :: lp = int64  !! Integer kind for step counters.
end module heatflow_kinds

A few rules that are easy to get wrong:

  • The doc block must be adjacent: a blank line between !> lines and the entity detaches it. A blank !> line inside the block is fine, it is a paragraph break.

  • For arguments, !! on the same line is the compact form; !> above a declaration also works but is longer. Both forms attach to the variable, and the documentation page lists them in the Arguments table with type, intent, optional and attributes.

  • Only entities FORD is told to display are documented. The display setting defaults to public and protected, so private helpers do not appear unless display includes private. This is why heatflow_kernels uses private by default with an explicit public list: the same declarations that make the module API small also make the generated documentation small.

  • Doc comments are comments to the compiler. They never cause a compile error.

The project file

FORD reads its settings from a project file. Two layouts are supported.

A Markdown file. The settings are YAML-like front matter between --- lines and the body becomes the front page of the site. This is the traditional layout (ford heatflow.md):

---
project: heatflow
summary: 2D heat diffusion on a rectangular plate
author: Heatflow authors
src_dir: ./src
         ./app
output_dir: ./doc
page_dir: ./pages
display: public
         protected
source: false
graph: true
search: true
preprocess: false
---

Heatflow solves the 2D heat equation with Jacobi and FTCS solvers.

@note This text is the project landing page and is written in Markdown.

A TOML table in fpm.toml. The options can also live in the [extra.ford] table of the fpm manifest, so a fpm project keeps one file. FORD reads it when it is given the manifest (ford fpm.toml), and prints Reading Ford options from /work/fordproj/fpm.toml. The manifest of the documented heatflow copy contains:

[extra.ford]
project = "heatflow"
summary = "2D heat diffusion on a rectangular plate"
src_dir = ["src", "app"]
output_dir = "doc-toml"
display = ["public", "protected"]
graph = true
source = false

fpm itself ignores [extra.<name>] tables, so adding the table does not change the build. The same names are available as command-line options for the common ones (ford --help lists -d/--src_dir, -p/--page_dir, -o/--output_dir, -s/--css, -r/--revision, --exclude, --exclude_dir, -e/--extensions, -m/--macro, -I/--include, -w/--warn, -g/--graph, --no-search, -q/--quiet, --debug, --config, -L/--external_links, --externalize); options given on the command line override the file.

The settings that matter most (defaults taken from the installed FORD 7.0.13 ProjectSettings):

Setting Default Meaning

src_dir

./src

directories scanned for source files

exclude, exclude_dir

none; doc output directory excluded

files and directories to skip

extensions

f90, f95, f03, f08, f15 and upper-case variants, plus F, FOR

extensions treated as free-form source

fixed_extensions

f, for, F, FOR

extensions treated as fixed form

extra_filetypes

none

additional file types and their comment character, for example shell scripts

display

public, protected

which access levels are documented (private can be added)

graph

false

generate call, uses and inheritance graphs (needs Graphviz)

source

false

include the source code in the site (incl_src is true: the sources are listed on a Source Files page)

search

true

build a client-side search index

preprocess

true

run the C-style preprocessor (pcpp) over files; set false if you do not use #if blocks

macro

none

preprocessor macros, for example macro: TEST

docmark, predocmark

!, >

the doc-comment markers (so !! and !>); the alternative marks docmark_alt and predocmark_alt also exist

page_dir

none

a directory of Markdown pages published beside the API reference

sort

src

ordering of lists (src keeps source order; alphabetical and type-based orders also exist)

warn

false

print a warning for each undocumented entity (same as -w)

externalize

true

write modules.json so other FORD projects can link to this one; external or -L imports another project

parallel

10

number of processes used while parsing

project_github, project_url, license, author, email, version, summary

empty

metadata shown in the site header and footer

graph_maxdepth, graph_maxnodes

very large

limits on graph size for big projects

mathjax_config

none

extra MathJax configuration

Running FORD

python3 -m venv .venv && .venv/bin/pip install FORD==7.0.13 fortls==3.2.2 fprettify
export PATH="$PWD/.venv/bin:$PATH"
ford heatflow.md

The real output (progress bars condensed to the last line of each stage):

  Parsing files         ━━━━━━━ 100% 12/12 0:00:00 0:00:… src/heatflow_kinds.f90
  Correlating information from different parts of your project...
  ...done in 0.001s
  Processing comments   ━━━━━━━━━━━━━━━━━━━━━━━━ 100% 211/211 0:00:00 0:00:00 lp
  Processing pages      ━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00 0:00:00 pages/index.md
  Creating HTML documentation... done in 0.000s
  Generating graphs     ━━━━━━━━━━━━━━━━ 100% 39/39 0:00:00 0:00:00 write_stream
  Creating search index ━━━━━━━━━━━━━ 100% 41/41 0:00:00 0:00:00 page/index.html
  Writing files         ━━━━━━━━━━━━━ 100% 48/48 0:00:00 0:00:00 doc/search.html

Browse the generated documentation: file:///work/fordproj/doc/index.html

The output directory is a static site that can be published as is (GitHub Pages, any web server). It contains one directory per kind of entity: module, proc, program, type, interface, namelist, blockdata, sourcefile, page, lists (index pages), and search.html with its index, plus modules.json for cross-project links. For heatflow, doc/module contains one page per heatflow_* module, doc/proc contains jacobi_sweep.html, ftcs_sweep.html, make_grid.html and the other procedures, and doc/type the derived types. The Arguments table of jacobi_sweep is generated from the declarations and the !! comments:

Arguments
Type                Intent  Name        Description
real(kind=dp)       in      u(:,:)      Current temperature field.
real(kind=dp)       inout   unew(:,:)   Updated field (interior points only).
real(kind=dp)       out     residual    Largest absolute change of any point.

(The columns are the text of the HTML table with the markup removed.) On the module page, the @note becomes a "Note" box and the TeX is typeset in the browser.

Graphs

With graph: true (or -g), FORD runs Graphviz and embeds SVG graphs: for each module, a Uses graph and a Used by graph; for procedures a Calls and Called by graph; for types, inheritance and containment; and for the whole project a module-dependency and a call graph. For heatflow_kernels the generated graphs say that it uses heatflow_kinds, which uses iso_fortran_env, and that it is used by heatflow_capi, heatflow_ftcs and heatflow_jacobi. This is the dependency picture that Modules and Submodules describes in prose, drawn automatically, which is a quick way to find a circular-looking design before the compiler complains. Without Graphviz installed, the graphs cannot be generated; install it (apt install graphviz, or the equivalent for your system) or set graph: false.

Undocumented entities

ford -w (or warn: true) prints one warning per undocumented entity. On the partly documented heatflow copy it starts like this:

Warning: Undocumented boundprocedure 'unknown' in file 'heatflow_grid.f90'
Warning: Undocumented variable 'nx' in file 'heatflow_grid.f90'
Warning: Undocumented variable 'ny' in file 'heatflow_grid.f90'
Warning: Undocumented variable 'lx' in file 'heatflow_grid.f90'
Warning: Undocumented proc 'unknown' in file 'heatflow_grid.f90'
Warning: Undocumented program 'unknown' in file 'main.f90'
Warning: Undocumented variable 'i' in file 'heatflow_kernels.f90'
Warning: Undocumented module 'unknown' in file 'heatflow_boundary.f90'

Two things stand out. The entity name is often printed as unknown for units, so the file name is the useful part. And local variables such as the loop indices i and j are reported too, so the list is a to-do list and not a target of zero; use it to find undocumented public entities, which display limits the site to anyway. Note that a missing warning does not mean the text is good: FORD only checks that a comment exists.

page_dir points at a directory of Markdown files with front matter (title: and optionally ordered_subpage:), which are published beside the API reference. Use pages for a user guide, an installation page and a theory page; use doc comments for the API. A comment can link to another entity with -style double brackets, which FORD resolves against the project (and against projects imported with external/-L, using the modules.json that externalize writes).

Practical notes

  • Run FORD in continuous integration and fail on errors, not on -w warnings, then publish the doc directory with the same pipeline as the site.

  • The documentation lives with the code: Fortran files are the single source, and FORD reads the same src directory as fpm.

  • Because the text is Markdown, put code samples in fenced blocks and keep lines readable in the source, where the comments are read most of the time.

  • The generated pages show the declarations as the compiler sees them; a private component with a public type is hidden unless display includes private.

The fortls language server

fortls is a language server for Fortran: a program that speaks the Language Server Protocol (LSP) over standard input and output, so any editor with an LSP client can offer code intelligence for Fortran without a plug-in per feature. It is a Python package (pip install fortls), maintained by the Fortran community. Probed with a real initialize request over stdio (a small Python script that frames the JSON-RPC message), fortls 3.2.2 advertises these capabilities:

completionProvider
definitionProvider
documentSymbolProvider
hoverProvider
implementationProvider
referencesProvider
renameProvider
textDocumentSync
workspaceSymbolProvider

fortls also answers textDocument/signatureHelp and publishes diagnostics, as the debug commands below show; the code-action support is marked experimental in its own help (--enable_code_actions). The editors with fortls instructions on the fortls site are Visual Studio Code, Atom, Sublime Text, Neovim, Vim (YouCompleteMe or LanguageClient-neovim), Emacs (LSP Mode or Eglot), Visual Studio 2017 and Kakoune.

Configuration

fortls reads .fortlsrc, .fortls.json or .fortls (JSON) from the project root, or the file named by -c/--config. Any --option of the command line can be a key. The configuration used for this page:

{
  "source_dirs": ["src", "app"],
  "excl_paths": ["build", "doc", "doc-toml"],
  "incl_suffixes": [".f90"],
  "lowercase_intrinsics": true,
  "hover_signature": true,
  "max_line_length": 100,
  "max_comment_line_length": 100,
  "disable_autoupdate": true,
  "enable_code_actions": false
}

The options from fortls --help, grouped as the help groups them:

Option Effect

--source_dirs, --excl_paths

folders containing sources and folders to skip

--incl_suffixes, --excl_suffixes

extra source extensions and excluded ones (defaults cover .F, .F77, .F90, .F95, .F03, .F08, .FOR, .FPP in lower and upper case)

-n/--nthreads

threads used to parse the workspace at start-up (default 4)

--incremental_sync

incremental document synchronisation (beta)

--sort_keywords

show attributes in a consistent order in hovers

--autocomplete_no_prefix, --autocomplete_no_snippets, --autocomplete_name_only, --use_signature_help

tune completion: no prefix filtering, no snippet placeholders, name only, or signature help instead of snippets

--lowercase_intrinsics

complete intrinsics and keywords in lower case

--hover_signature, --hover_language

show argument signatures in hovers; language id used for hover blocks (default fortran90)

--max_line_length, --max_comment_line_length, --disable_diagnostics

line-length diagnostics, or turn diagnostics off

--pp_suffixes, --include_dirs, --pp_defs

preprocessor handling: extensions parsed only for preprocessor commands, include folders, extra definitions as JSON

--symbol_skip_mem

leave type members out of document symbols

--enable_code_actions

experimental code actions

--disable_autoupdate

fortls checks PyPI for newer versions and installs them by default; this switch stops that

--debug_log

write a debug log in the project root

The --disable_autoupdate option deserves attention in a managed environment: the default behaviour of a tool that updates itself from the network is often not what a reproducible workspace wants, so pin the version and disable the update.

What it can do, shown by the debug commands

fortls has command-line tests for every request (fortls --debug_help lists them), which are useful to check a configuration without an editor. They need --debug_rootpath so the workspace is loaded, and take 1-indexed --debug_line and --debug_char. All of the following are real output on the documented heatflow copy (the repeated "Testing" header is trimmed).

Document symbols (the outline view):

$ fortls -c .fortls.json --debug_filepath src/heatflow_kernels.f90 --debug_symbols
  line     9  symbol ->   2:heatflow_kernels               parent = null
  line    20  symbol ->  12:jacobi_sweep                   parent = heatflow_kernels
  line    37  symbol ->  12:ftcs_sweep                     parent = heatflow_kernels

Hover on make_grid in app/main.f90 (line 22): the signature and the !> doc comment, which is why FORD-style comments pay twice, in the generated site and in the editor:

FUNCTION make_grid(nx, ny, lx, ly, initial=initial) RESULT(g)
 INTEGER(ip), INTENT(IN) :: nx
 INTEGER(ip), INTENT(IN) :: ny
 REAL(dp), INTENT(IN) :: lx
 REAL(dp), INTENT(IN) :: ly
 REAL(dp), INTENT(IN), OPTIONAL :: initial
 TYPE(grid_t) :: g
-----
Create a grid covering `lx` by `ly`, optionally filled with `initial`.

Go to definition from the call make_grid (line 22, character 12) and from the type-bound call grid%apply_boundary (line 23):

    URI  = "file:///work/fordproj/src/heatflow_grid.f90"
    Line = 27
    Char = 12

    URI  = "file:///work/fordproj/src/heatflow_grid.f90"
    Line = 17
    Char = 18

The second result is the procedure :: apply_boundary => grid_apply_boundary binding line inside grid_t, not the body of the subroutine: fortls resolves a type-bound call to the binding.

References to the variable residual (declared on line 13):

  file:///work/fordproj/app/main.f90  (13, 15)
  file:///work/fordproj/app/main.f90  (26, 49)
  file:///work/fordproj/app/main.f90  (27, 69)

Rename residual to resid, a workspace-wide edit (shown as the diff the server returns):

File: "/work/fordproj/app/main.f90"
  13, 13
  -   real(dp) :: residual
  +   real(dp) :: resid

  26, 26
  -   call solver%run(cfg%max_steps, cfg%tolerance, residual)
  +   call solver%run(cfg%max_steps, cfg%tolerance, resid)

  27, 27
  -   write (*, '(a,i0,a,es9.2)') 'steps=', solver%steps, ' residual=', residual
  +   write (*, '(a,i0,a,es9.2)') 'steps=', solver%steps, ' residual=', resid

Note that the string ' residual=' on line 27 is correctly left alone: rename works on symbols, not text.

Completion after grid% lists the type-bound procedures of grid_t, with the generic write(formatted) and the specific binding write_formatted (declared private in the type, but still offered by fortls):

    3: apply_boundary -> SUBROUTINE apply_boundary(bc, value)
    3: set_hot_spot -> SUBROUTINE set_hot_spot(temperature)
    3: max_temperature -> REAL(dp) FUNCTION max_temperature()
    2: write(formatted) -> SUBROUTINE write(formatted)(unit, iotype, v_list, iostat, iomsg)
    3: write_formatted -> SUBROUTINE write_formatted(unit, iotype, v_list, iostat, iomsg)

Signature help on the second argument of make_grid reports the active parameter and the argument list:

    Active param = 1
    Active sig   = 0
    make_grid(nx, ny, lx, ly, initial=initial)

Workspace symbols for the query sweep find both kernels:

  heatflow_kernels::37  symbol -> ftcs_sweep                     parent = src/heatflow_kernels.f90
  heatflow_kernels::20  symbol -> jacobi_sweep                   parent = src/heatflow_kernels.f90

Diagnostics are deliberately limited. On a module with a missing use target and a duplicate declaration, fortls reports:

      2:INFO  "Module "missing_module" not found in project"
      9:ERROR  "Variable "x" declared twice in scope"

and on a line longer than the configured max_line_length, a WARNING that the line exceeds 100 characters. It did not report an unused local variable or a misspelled name, because fortls parses the source and does not compile it: it has no type checker. Treat its diagnostics as editing aids and let the compiler (see Compilers and Compiler Options) be the authority. In the same way, an implementation request on a deferred binding (step in solver_t) returned "No result found" in this test, so do not rely on that request for navigating from an abstract type to its extensions.

Limits to know about

  • fortls works from the source files and its own parser, with no knowledge of the compiler flags. Preprocessor-heavy code needs --pp_suffixes, --include_dirs and --pp_defs, and the .F90 extensions are by default parsed with the preprocessor.

  • It does not read compile_commands.json: a search of the installed fortls package for that file name finds nothing, so the server finds its sources only through source_dirs, the working root and the suffix options. Keep source_dirs in sync with the build system.

  • Modules from dependencies that live outside the workspace (for example stdlib built by fpm into build/dependencies) are found only if their sources are inside a source_dirs entry; otherwise the INFO diagnostic "Module …​ not found in project" appears and completion for that module is missing.

  • The language server cannot replace the compiler’s interface checks, and it does not know which Fortran standard you target.

Formatters and linters

The tools in this section are community tools, not part of any compiler. They are useful, but always review their diffs before committing, and keep a test run in the same change (see the modernisation workflow in Legacy Features and Modernization).

The input used below is a deliberately badly formatted copy of a smoothing routine (upper-case keywords, no spaces, no indentation):

MODULE Messy_Demo
USE heatflow_kinds, ONLY: dp
IMPLICIT NONE
CONTAINS
SUBROUTINE smooth(u,n,omega)
INTEGER,INTENT(IN)::n
REAL(dp),INTENT(INOUT)::u(n)
REAL(dp),INTENT(IN)::omega
INTEGER::i
DO i=2,n-1
IF(omega>0.0_dp)THEN
u(i)=u(i)+omega*(u(i-1)-2*u(i)+u(i+1))
ELSE
u(i)=u(i)
END IF
END DO
END SUBROUTINE smooth
END MODULE Messy_Demo

fprettify

fprettify (by Patrick Seewald, Python, pip install fprettify) reformats modern free-form Fortran: indentation, whitespace around operators and commas, line length, and optionally the letter case of keywords. It reads configuration files named .fprettify.rc in the home directory and in the parent directories of the file, or the file named with -c. Its options (from fprettify --help) include -i/--indent (default 3), -l/--line-length (default 132), -w/--whitespace presets 0 to 4 (default 2) and the individual --whitespace-comma, --whitespace-assignment and the other --whitespace-<kind> switches, --strict-indent, --enable-decl, --disable-indent, --disable-whitespace, --enable-replacements, --c-relations, --case, --strip-comments, --disable-fypp, --disable-indent-mod, -d/--diff, -s/--stdout, -S/--silent, -r/--recursive, -e/--exclude and -f/--fortran.

With the defaults (output to standard output, fprettify --stdout messy.f90):

MODULE Messy_Demo
   USE heatflow_kinds, ONLY: dp
   IMPLICIT NONE
CONTAINS
   SUBROUTINE smooth(u, n, omega)
      INTEGER, INTENT(IN)::n
      REAL(dp), INTENT(INOUT)::u(n)
      REAL(dp), INTENT(IN)::omega
      INTEGER::i
      DO i = 2, n - 1
      IF (omega > 0.0_dp) THEN
         u(i) = u(i) + omega*(u(i - 1) - 2*u(i) + u(i + 1))
      ELSE
         u(i) = u(i)
      END IF
      END DO
   END SUBROUTINE smooth
END MODULE Messy_Demo

Two surprises in this real output, both documented behaviour:

  • The if inside the do is not indented. By default fprettify is "relaxed" about nested loops; --strict-indent ("strictly impose indentation even for nested loops") indents every nesting level.

  • The :: separators in declarations are left alone, because spacing of declarations is off unless --enable-decl is given.

With a .fprettify.rc next to the file:

indent = 2
strict-indent = true
enable-decl = true
whitespace = 2
line-length = 100

and --case 1 1 1 1 on the command line (the four values are keywords, procedures and modules, operators, and constants; 0 leaves as is, 1 lower-cases, 2 upper-cases), the output is:

module Messy_Demo
  use heatflow_kinds, only: dp
  implicit none
contains
  subroutine smooth(u, n, omega)
    integer, intent(IN) :: n
    real(dp), intent(INOUT) :: u(n)
    real(dp), intent(IN) :: omega
    integer :: i
    do i = 2, n - 1
      if (omega > 0.0_dp) then
        u(i) = u(i) + omega*(u(i - 1) - 2*u(i) + u(i + 1))
      else
        u(i) = u(i)
      end if
    end do
  end subroutine smooth
end module Messy_Demo

The case option lower-cased the statement keywords but left the attribute arguments IN and INOUT and the name Messy_Demo as written. Running fprettify a second time on the result changes nothing, so it is safe to run on save and in a pre-commit hook. In this test, a case line in the .fprettify.rc was not honoured (the keywords stayed upper case, and some spellings of the value made fprettify print its usage text), while the same setting on the command line worked, so pass --case on the command line.

Use fprettify -d file.f90 to see the diff instead of rewriting, and fprettify -r src -e build to process a tree. fprettify cannot convert fixed-form source and may leave lines it cannot break; always compile afterwards.

findent

findent (a command-line program maintained by Willem Vermin, packaged in Debian as findent) indents Fortran and, unlike fprettify, handles fixed form and can convert between the two. It reads standard input and writes standard output. On the same input, the default run changes indentation but not spacing or case:

MODULE Messy_Demo
   USE heatflow_kinds, ONLY: dp
   IMPLICIT NONE
CONTAINS
   SUBROUTINE smooth(u,n,omega)
      INTEGER,INTENT(IN)::n
      REAL(dp),INTENT(INOUT)::u(n)
      REAL(dp),INTENT(IN)::omega
      INTEGER::i
      DO i=2,n-1
         IF(omega>0.0_dp)THEN
            u(i)=u(i)+omega*(u(i-1)-2*u(i)+u(i+1))
         ELSE
            u(i)=u(i)
         END IF
      END DO
   END SUBROUTINE smooth
END MODULE Messy_Demo

The useful part for older code is conversion. Given a fixed-form FORTRAN 77 routine, findent -ifixed -ofree produces free form and moves labels to the left (-Rr also rewrites a bare END to end subroutine <name>):

      SUBROUTINE AXPY(N,A,X,Y)
      INTEGER N
      DOUBLE PRECISION A,X(N),Y(N)
      DO 10 I=1,N
         Y(I)=Y(I)+A*X(I)
   10 CONTINUE
      RETURN
      END
$ findent -ifixed -ofree -Rr < old.f
SUBROUTINE AXPY(N,A,X,Y)
   INTEGER N
   DOUBLE PRECISION A,X(N),Y(N)
   DO 10 I=1,N
      Y(I)=Y(I)+A*X(I)
10 CONTINUE
   RETURN
end subroutine AXPY

The result passed gfortran -fsyntax-only -Wall in the container. The conversion keeps the obsolescent labelled do and the implicit typing: it changes the form, not the style (that is the work of the steps in Legacy Features and Modernization). Other options from findent -h: -i<n> indent width, -ifree/-ifixed/-iauto input format, -ofree/-ofixed/-osame output format, -RR for upper-case END SUBROUTINE name, --relabel to renumber labels, -q to guess whether a file is free or fixed, --ws_remred to collapse redundant white space, --openmp to control indentation of OpenMP conditional lines, and --continuation=<c> for the continuation character. A comment line ! findentfix: <statement> corrects the indentation when findent misjudges a construct. The Debian package also ships a wfindent wrapper script, and findent -lastindent is meant for use from Vim.

Fortitude and other linters

Fortitude (pip install fortitude-lint, a linter written in Rust, installed here as 0.9.2) is a static analyser in the style of ruff for Python. It reports rule violations with codes, can apply fixes with --fix, and has output formats for CI (github, gitlab, junit, sarif, json and more). On the messy module:

messy.f90:1:1: C131 module 'Messy_Demo' missing default accessibility statement
messy.f90:3:1: C003 'implicit none' missing 'external'
  = help: Add `(external)` to 'implicit none'

fortitude: 1 files scanned.
Number of errors: 2

Both rules are exactly the habits recommended in Best Practices and Style: a private default for modules and implicit none (type, external). Run over the heatflow sources with --statistics, the findings were S241 bad-quote-string (43 occurrences, auto-fixable, the single versus double quote convention), C003 (12), C002 implicit typing in an interface (4), C072 assumed-size character intent (2), C181 unchecked stat (2) and C011 missing default case (1). --target-std sets the minimum standard (f95, f2003, f2008, f2018 default, f2023), --select/--ignore choose rules, and fortitude explain C003 documents a rule. Fortitude is young and its rule set changes between releases, so pin its version in CI.

The most reliable linter remains the compiler itself. gfortran -fsyntax-only -std=f2023 -Wall -Wextra -pedantic -fimplicit-none (and the equivalents of other compilers) checks types, interfaces and standard conformance, which no external linter does; see Compilers and Compiler Options for the flag tables. Other community formatters and linters exist; they were not installed for this page, so nothing is claimed about them.

compile_commands.json

compile_commands.json is the JSON compilation database format that was defined by the Clang project: an array of objects with directory, file and either command or arguments, one object per source file compiled. Tools use it to learn the exact flags and include paths used to compile each file. Fortran build systems can emit it too, which is useful for any tool that asks for it. The three build systems of this guide produce it in these ways, all verified on the heatflow project.

fpm writes build/compile_commands.json on every fpm build:

{
  "arguments": [
    "gfortran", "-c", "././src/heatflow_boundary.f90",
    "-fimplicit-none", "-Werror=implicit-interface", "-ffree-form",
    "-I././include", "-J", "build/gfortran_DD609D2CDA6636FC",
    "-Ibuild/gfortran_DD609D2CDA6636FC",
    "-o", "build/gfortran_DD609D2CDA6636FC/heatflow/src_heatflow_boundary.f90.o"
  ],
  "directory": "/work/hf",
  "file": "././src/heatflow_boundary.f90"
}

The project had 13 entries (the 11 library modules, the app and the dependent files). The paths are relative to directory, and fpm build --help documents the file under the --list option ("the build sequence is saved to <build-dir>/compile_commands.json").

CMake writes it with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON; the file is compile_commands.json in the build directory. Both the Ninja and the Unix Makefiles generators produced it for the Fortran project. The first of the 14 entries with Ninja:

/usr/local/bin/gfortran -I/work/hf/bld/modules -I/work/hf/include -Jmodules -c /work/hf/src/heatflow_boundary.f90 -o CMakeFiles/heatflow.dir/src/heatflow_boundary.f90.o

Meson writes compile_commands.json in the build directory on every meson setup without an option (mb/compile_commands.json), with 12 entries for this project. The first entry’s command:

gfortran -Ilibheatflow.a.p -I. -I.. -fdiagnostics-color=always -D_FILE_OFFSET_BITS=64 -Wall -Wextra -O0 -g -fPIC -Jlibheatflow.a.p -o libheatflow.a.p/src_heatflow_kinds.f90.o -c ../src/heatflow_kinds.f90

Three practical points. First, the compiler flags and paths are the build system’s, so the file is a way to reproduce a compile of one file (for example -fsyntax-only with the same -I and -J options) and to feed analysis tools. Second, the module output directory (-J) and the include directories matter because a Fortran file cannot be analysed without the .mod files of the modules it uses; the database tells a tool where they are. Third, the language server above does not read this file (see "Limits to know about"), so do not expect a symlink of compile_commands.json to the project root to improve fortls. Which of the other tools in your editor use it depends on the tool; check its documentation, as this page does not claim any.

Community resources

  • The fortran-lang community site (https://fortran-lang.org/) hosts the tutorials, the style guide and best practices used throughout this guide, the compiler list and the learning resources, and it is the home of the packages mentioned above (fpm, stdlib, FORD, fortls, fprettify, test-drive).

  • The fortran-lang Discourse (https://fortran-lang.discourse.group/) is the discussion forum: questions about compilers and the standard, release announcements for the tools above, and the discussions behind proposals for the next standard.

  • The Fortran package index (https://fortran-lang.org/packages/) lists packages (libraries, tools and applications) that are usable with fpm, with their source repositories. Check licence and maintenance before depending on an entry, as for any package index.

  • The GitHub organisation fortran-lang hosts the sources and issue trackers of the tools in this page; reporting a formatter or language-server issue with a minimal file is usually the fastest way to get it fixed.

Where older material is outdated

Older Fortran material, such as Fortran 77 and early Fortran 90 tutorials, predates all of this tooling:

  • Documentation. Old projects document with free-form comments, Doxygen configuration with Fortran filters, or not at all. FORD understands modules, derived types and type-bound procedures, which Doxygen filters of the Fortran 77 era did not.

  • Editors. Older books assume a vendor IDE or a plain editor with column rulers for fixed form. A language server gives navigation, rename and hover in any LSP-capable editor for free-form code; fixed-form code is a weak case for fortls and best converted first (findent).

  • Formatting. Fixed-form tools such as column checkers do not apply to free form. fprettify and findent are the current community tools; the standard has no formatting rules.

  • Build metadata. Older makefiles carry compile flags in the makefile only. Modern builds can export a compilation database for tools.

  • Version baseline. Tool behaviour above is for the versions named at the top of the page; both fprettify’s --strict-indent default and fortls’s --disable_autoupdate behaviour are examples of settings that changed or may change, so read the --help of the version you install.