Skip to content
Open

Lint #1332

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d4956c2
test
arnaudlevy Feb 10, 2026
c883df2
test
alexisben Feb 10, 2026
652ff13
test lint
alexisben Feb 10, 2026
662ac5b
message
arnaudlevy Feb 10, 2026
28fc5a7
Merge branch 'lint' of github.com:osunyorg/theme into lint
arnaudlevy Feb 10, 2026
4f1b65b
python
arnaudlevy Feb 10, 2026
51d3fec
test
arnaudlevy Feb 10, 2026
c0d67df
test
arnaudlevy Feb 10, 2026
a6eb820
test
arnaudlevy Feb 10, 2026
94411a1
test
arnaudlevy Feb 10, 2026
a320e8d
test
arnaudlevy Feb 10, 2026
d002884
test
arnaudlevy Feb 10, 2026
17d71a9
test
arnaudlevy Feb 10, 2026
be9fe2c
test
arnaudlevy Feb 10, 2026
fa6df01
test
arnaudlevy Feb 10, 2026
5518f92
test
arnaudlevy Feb 10, 2026
307e3ca
test
arnaudlevy Feb 10, 2026
c092607
better
arnaudlevy Feb 10, 2026
cb46c05
ignore
arnaudlevy Feb 10, 2026
57a6fe6
complexity
arnaudlevy Feb 10, 2026
a18f47c
ruby
arnaudlevy Feb 10, 2026
4ea297e
table
arnaudlevy Feb 10, 2026
30da7e0
test
arnaudlevy Feb 11, 2026
b58a95c
test
arnaudlevy Feb 11, 2026
7714de6
refactor
arnaudlevy Feb 11, 2026
f761f0f
refactor
arnaudlevy Feb 11, 2026
5bbc261
refactor
arnaudlevy Feb 11, 2026
bbfd278
refac
arnaudlevy Feb 11, 2026
befad32
lines
arnaudlevy Feb 11, 2026
2b219b3
cpx
arnaudlevy Feb 11, 2026
bb665de
wip
arnaudlevy Feb 11, 2026
6a28707
messages
arnaudlevy Feb 11, 2026
d332163
title
arnaudlevy Feb 11, 2026
c66e706
wip
arnaudlevy Feb 12, 2026
ea17a7e
call
arnaudlevy Feb 12, 2026
2841036
exclusions
arnaudlevy Feb 12, 2026
f19bb0b
lint
arnaudlevy Feb 12, 2026
82ada46
calls
arnaudlevy Feb 12, 2026
f5a3200
fix
arnaudlevy Feb 12, 2026
f3c780b
Merge branch 'main' into lint
arnaudlevy Feb 12, 2026
9993386
credit
arnaudlevy Feb 12, 2026
fa35a6e
llint
arnaudlevy Feb 12, 2026
0eb2654
lines
arnaudlevy Feb 13, 2026
8fdf05f
helper calls
arnaudlevy Feb 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .djlintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"profile": "golang",
"indent": 2,
"max_line_length": 120,
"format_css": true,
"format_js": true,
"exclude": "node_modules,public,resources,static,.git",
"ignore": "H006,H013,H016,H021,H030,H031,T001,T002",
"custom_blocks": "define,block,range,with,if,else,end",
"linter_output_format": "### {message}\n{filename}, line {line}, code {code} \n```html\n{match}\n```",
"require_pragma": false,
"preserve_blank_lines": true,
"max_blank_lines": 2
}
44 changes: 44 additions & 0 deletions .github/workflows/analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Hugo Static Analysis

on: push

permissions:
contents: read
pull-requests: write
checks: write

jobs:
template-lint:
name: Lint Hugo Templates
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'

- name: Install djLint
run: pip install djlint

- name: Lint layouts
run: |
echo "## djLint" >> report.txt
djlint layouts/ >> report.txt || true

- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.4.1'

- name: Complexity
run: |
ruby bin/hugolint.rb >> report.txt || true

- name: Comment pull request
uses: mshick/add-pr-comment@v2
with:
message-path: |
report.txt
12 changes: 12 additions & 0 deletions .hugolint
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
engines:
directories:
exclude:
- partials/blocks/templates
complexity:
exclude:
lines:
exclude:
calls:
exclude:
- partials/projects/single/sidebar.html
- partials/blocks/templates/categories.html
2 changes: 2 additions & 0 deletions bin/hugolint.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require_relative 'hugolint/analyzer'
puts Hugolint::Analyzer.run
41 changes: 41 additions & 0 deletions bin/hugolint/analyzer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require_relative 'engines/base'
require_relative 'engines/complexity'
require_relative 'engines/directories'
require_relative 'engines/lines'
require_relative 'engines/calls'
require_relative 'file'
require_relative 'utils'
require 'yaml'

module Hugolint
class Analyzer

LAYOUTS = './layouts/**/*'
CONFIG = '.hugolint'

def self.run
new.to_s
end

def to_s
message = "## Hugo analyzer\n"
message += Engines::Directories.new(self).to_s
message += Engines::Calls.new(self).to_s
message += Engines::Lines.new(self).to_s
message += Engines::Complexity.new(self).to_s
message
end

def paths
@paths ||= Dir.glob(LAYOUTS)
end

def files
@files ||= paths.map { |path| Hugolint::File.new(path) }
end

def config
@config ||= YAML.load_file(CONFIG)
end
end
end
54 changes: 54 additions & 0 deletions bin/hugolint/engines/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Hugolint
module Engines
class Base
attr_reader :analyzer

ICON_DANGER = '❌'
ICON_WARNING = '⚠️'
ICON_OK = '✅'

def initialize(analyzer)
@analyzer = analyzer
end

def analyzed_files
unless @analyzed_files
@analyzed_files = []
analyzer.files.each do |file|
next unless should_analyze?(file)
@analyzed_files << analyze(file)
end
sort!
end
@analyzed_files
end

def to_s
end

protected

def should_analyze?(file)
!files_excluded.include?(file.short_path)
end

def analyze(file)
end

def sort!
end

def engine_identifier
self.class.name.split('::').last.downcase
end

def engine_config
@engine_config ||= analyzer.config.dig('engines', engine_identifier)
end

def files_excluded
@files_excluded ||= engine_config&.dig('exclude') || []
end
end
end
end
62 changes: 62 additions & 0 deletions bin/hugolint/engines/calls.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Hugolint
module Engines
class Calls < Base

ROOT = './layouts/partials/'

def to_s
message = "### Partials calls\n"
message += "Partials called once might be in the wrong place. Partials never called might be metaprogrammed, or obsolete.\n"
message += "| Id | State | Calls | Fragment | Partial |\n"
message += "|---|---|---|---|---|\n"
index = 1
analyzed_files.each do |file|
calls = file.json[:calls]
next unless calls[:problem]
message += "| cal-#{index} | #{calls[:icon]} | #{calls[:count]} | #{calls[:fragment]} | #{file.short_path} |\n"
index += 1
end
message
end

protected

def should_analyze?(file)
super &&
!file.directory? &&
file.path.include?(ROOT)
end

def analyze(file)
fragment = file.path.gsub(ROOT, '').gsub('.html', '')
call = "partial \"#{fragment}"
count = Hugolint::Utils.occurrences_in_files(call, analyzer.files)
# Les fichiers ont le droit d'être utilisés 1 seule fois,
# si et seulement si ce ne sont pas des helpers à la racine
is_helper = !fragment.include?('/')
if count == 0
problem = true
icon = ICON_DANGER
elsif count == 1 && is_helper
problem = true
icon = ICON_WARNING
else
problem = false
icon = ICON_OK
end
file.json[:calls] = {
fragment: fragment,
count: count,
problem: problem,
icon: icon
}
file
end

def sort!
@analyzed_files.sort_by! { |file| file.json[:calls][:count] }
.reverse!
end
end
end
end
63 changes: 63 additions & 0 deletions bin/hugolint/engines/complexity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Hugolint
module Engines
class Complexity < Base

KEYWORDS = [
'if',
'else',
'for',
'range',
'with',
].freeze

DANGER = 10
WARNING = 5

def to_s
message = "### Complexity\n"
message += "Cyclomatic complexity should not be too high.\n"
message += "| Id | State | Complexity | File |\n"
message += "|---|---|---|---|\n"
index = 1
analyzed_files.each do |file|
complexity = file.json[:complexity]
next unless complexity[:problem]
message += "| cpx-#{index} | #{complexity[:icon]} | #{complexity[:score]} | #{file.short_path} |\n"
index += 1
end
message
end

protected

def should_analyze?(file)
super &&
!file.directory?
end

def sort!
@analyzed_files.sort_by!{ |file| file.json[:complexity][:score] }.reverse!
end

def analyze(file)
score = 1
KEYWORDS.each do |keyword|
score += Hugolint::Utils.occurences("#{keyword} ", file.data)
end
problem = score > WARNING
icon = ICON_OK
if score > DANGER
icon = ICON_DANGER
elsif score > WARNING
icon = ICON_WARNING
end
file.json[:complexity] = {
score: score,
icon: icon,
problem: problem
}
file
end
end
end
end
53 changes: 53 additions & 0 deletions bin/hugolint/engines/directories.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module Hugolint
module Engines
class Directories < Base

DANGER = 20
WARNING = 10

def to_s
message = "### Directories \n"
message += "Directories should not contain too many files, it's probably a sign of mess.\n"
message += "| Id | State | Files | Directory |\n"
message += "|---|---|---|---|\n"
index = 1
analyzed_files.each do |file|
directory = file.json[:directory]
next unless directory[:problem]
message += "| dir-#{index} | #{directory[:icon]} | #{directory[:count]} | #{file.short_path} |\n"
index += 1
end
message
end

protected

def should_analyze?(file)
super &&
file.directory?
end

def analyze(file)
count = Dir["#{file.path}/*.*"].length
problem = count > WARNING
icon = ICON_OK
if count > DANGER
icon = ICON_DANGER
elsif count > WARNING
icon = ICON_WARNING
end
file.json[:directory] = {
count: count,
icon: icon,
problem: problem
}
file
end

def sort!
@analyzed_files.sort_by! { |file| file.json[:directory][:count] }
.reverse!
end
end
end
end
Loading