mirror of
https://github.com/discourse/discourse.git
synced 2025-10-03 17:21:20 +08:00
DEV: update various ai agent configurations (#34192)
Some checks are pending
Licenses / run (push) Waiting to run
Linting / run (push) Waiting to run
Publish Assets / publish-assets (push) Waiting to run
Tests / core backend (push) Waiting to run
Tests / plugins backend (push) Waiting to run
Tests / core frontend (Chrome) (push) Waiting to run
Tests / plugins frontend (push) Waiting to run
Tests / themes frontend (push) Waiting to run
Tests / core system (push) Waiting to run
Tests / plugins system (push) Waiting to run
Tests / themes system (push) Waiting to run
Tests / core frontend (Firefox ESR) (push) Waiting to run
Tests / core frontend (Firefox Evergreen) (push) Waiting to run
Tests / chat system (push) Waiting to run
Tests / merge (push) Blocked by required conditions
Some checks are pending
Licenses / run (push) Waiting to run
Linting / run (push) Waiting to run
Publish Assets / publish-assets (push) Waiting to run
Tests / core backend (push) Waiting to run
Tests / plugins backend (push) Waiting to run
Tests / core frontend (Chrome) (push) Waiting to run
Tests / plugins frontend (push) Waiting to run
Tests / themes frontend (push) Waiting to run
Tests / core system (push) Waiting to run
Tests / plugins system (push) Waiting to run
Tests / themes system (push) Waiting to run
Tests / core frontend (Firefox ESR) (push) Waiting to run
Tests / core frontend (Firefox Evergreen) (push) Waiting to run
Tests / chat system (push) Waiting to run
Tests / merge (push) Blocked by required conditions
Shortened AI agent file, so we conserve tokens Use symlinks for all agent files where needed to avoid round trips to llm Added config for Open AI Codex / Gemini and Cursor
This commit is contained in:
parent
df03ef6d05
commit
e6f9cde35e
8 changed files with 463 additions and 130 deletions
7
.cursor/rules/ai-agents-always.mdc
Normal file
7
.cursor/rules/ai-agents-always.mdc
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
description: Always include our AI agents guide
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
@ai-agents.md
|
1
.github/instructions/copilot.instructions.md
vendored
Symbolic link
1
.github/instructions/copilot.instructions.md
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
AI-AGENTS.md
|
1
AGENTS.md
Symbolic link
1
AGENTS.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
AI-AGENTS.md
|
198
AI-AGENTS.md
198
AI-AGENTS.md
|
@ -1,156 +1,96 @@
|
|||
# AI Coding Agent Guide
|
||||
|
||||
This file contains project-specific instructions that any AI coding agent should read at the start of each conversation and maintain in memory throughout the entire interaction. IMPORTANT: Once this file has been read or updated, it MUST be loaded at the beginning of any new conversation to ensure awareness of communication requirements, custom tasks, etc.
|
||||
Project-specific instructions for AI agents. MUST be loaded at conversation start.
|
||||
|
||||
## Default Mode
|
||||
- Architect mode enabled by default: detailed analysis, patterns, trade-offs, architectural guidance
|
||||
- Stop and ask for context if unable to write code meeting guidelines
|
||||
|
||||
- Architect mode should be enabled by default.
|
||||
- Focus on providing detailed analysis, patterns, trade-offs, and architectural guidance.
|
||||
- If you're unable to write code that fits these guidelines, stop and ask for additional context from the developer.
|
||||
## Development Rules
|
||||
Discourse is large with long history. Understand context before changes.
|
||||
|
||||
## Development Environment
|
||||
### All Files
|
||||
- Always lint changed files
|
||||
- Make display strings translatable (use placeholders, not split strings)
|
||||
- Create subagent to review changes against this file after completing tasks
|
||||
|
||||
Discourse is a large project with a long development history. Ensure you understand the context of any changes you're making before you start.
|
||||
|
||||
### General Rules
|
||||
These rules apply to ALL files being changed.
|
||||
|
||||
- Always lint changed files.
|
||||
- Always make display strings translatable.
|
||||
- Avoid splitting display strings into pieces: use translation placeholders, or multiple translatable strings where appropriate.
|
||||
- After completing a task, create a subagent to review the changes and ensure they conform to the instructions in this file, as well as the prompt(s) given.
|
||||
### Toolset
|
||||
- Use `pnpm` for JavaScript, `bundle` for Ruby
|
||||
- Use helpers in bin over bundle exec (bin/rspec, bin/rake)
|
||||
|
||||
### JavaScript
|
||||
- Don't create empty backing classes for template tag only components, unless specifically asked to.
|
||||
- Use the FormKit library for creating forms and form inputs. FormKit is documented here: https://meta.discourse.org/t/discourse-toolkit-to-render-forms/326439 and defined in `app/assets/javascripts/discourse/app/form-kit`
|
||||
- No empty backing classes for template-only components unless requested
|
||||
- Use FormKit for forms: https://meta.discourse.org/t/discourse-toolkit-to-render-forms/326439 (`app/assets/javascripts/discourse/app/form-kit`)
|
||||
|
||||
### JavaScript Documentation
|
||||
- Always add JSDocs for classes, methods, and members, except for:
|
||||
- `@service` members
|
||||
- constructors
|
||||
- Always use multiline JSDoc format.
|
||||
- For components:
|
||||
- Specify the component name with `@component`.
|
||||
- List the params. These can be found in `this.args` in the JS, or `@paramname` in the `<template>`.
|
||||
- For methods:
|
||||
- Don't add `@returns` for `@action` methods.
|
||||
- Don't add `@type` for getters, document with `@returns`.
|
||||
- For members:
|
||||
- Specify the @type.
|
||||
### JSDoc
|
||||
- Required for classes, methods, members (except `@service` members, constructors)
|
||||
- Multiline format only
|
||||
- Components: `@component` name, list params (`this.args` or `@paramname`)
|
||||
- Methods: no `@returns` for `@action`, use `@returns` for getters (not `@type`)
|
||||
- Members: specify `@type`
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### General Rules
|
||||
- Don't write tests for functionality that is handled by classes/components/modules other than the specific one being tested.
|
||||
- Don't write obvious tests (eg, testing that a string can contain unicode characters)
|
||||
|
||||
### Ruby Test Rules
|
||||
- Use `fab!()` instead of `let()` wherever possible.
|
||||
- We use system tests in rails for UI integration testing, which is documented at https://dev.discourse.org/t/systematic-system-specs/82525, and examples are in `spec/system`
|
||||
- We use page objects in system specs, defined in `spec/system/page_objects`
|
||||
|
||||
### Command Reference
|
||||
|
||||
#### Testing
|
||||
## Testing
|
||||
- Don't test functionality handled by other classes/components
|
||||
- Don't write obvious tests
|
||||
- Ruby: use `fab!()` over `let()`, system tests for UI (`spec/system`), page objects (`spec/system/page_objects`)
|
||||
|
||||
### Commands
|
||||
```bash
|
||||
# Run all Ruby tests
|
||||
bin/rspec
|
||||
# Ruby tests
|
||||
bin/rspec [spec/path/file_spec.rb[:123]]
|
||||
LOAD_PLUGINS=1 bin/rspec # Plugin tests
|
||||
|
||||
# Run a specific Ruby test file
|
||||
bin/rspec spec/path/to/file_spec.rb
|
||||
# JavaScript tests
|
||||
bin/rake qunit:test # RUN all non plugin tests
|
||||
LOAD_PLUGINS=1 TARGET=all FILTER='fill filter here' bin/rake qunit:test # RUN specific tests based on filter
|
||||
|
||||
# Run a specific Ruby test by line number
|
||||
bin/rspec spec/path/to/file_spec.rb:123
|
||||
Exmaple filters JavaScript tests:
|
||||
|
||||
# Run JavaScript tests
|
||||
bin/rake qunit:test
|
||||
emoji-test.js
|
||||
...
|
||||
acceptance("Emoji" ..
|
||||
test("cooked correctly")
|
||||
...
|
||||
Filter string is: "Acceptance: Emoji: cooked correctly"
|
||||
|
||||
# Run a specific JavaScript test module
|
||||
pnpm ember exam --filter 'Module | Filter | goes-here'
|
||||
user-test.js
|
||||
...
|
||||
module("Unit | Model | user" ..
|
||||
test("staff")
|
||||
...
|
||||
Filter string is: "Unit | Model | user: staff"
|
||||
|
||||
# Linting
|
||||
bin/lint path/to/file path/to/another/file
|
||||
bin/lint --fix path/to/file path/to/another/file
|
||||
bin/lint --fix --recent # Lint all recently changed files
|
||||
```
|
||||
|
||||
#### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Lint Ruby files
|
||||
bundle exec rubocop path/to/file
|
||||
bundle exec stree write Gemfile path/to/file
|
||||
|
||||
# Lint JavaScript/TypeScript files
|
||||
pnpm lint:js path/to/file
|
||||
pnpm lint:hbs path/to/file
|
||||
pnpm lint:prettier path/to/file
|
||||
|
||||
# Lint CSS/SCSS files
|
||||
pnpm lint:css path/to/file
|
||||
```
|
||||
ALWAYS lint any changes you make
|
||||
|
||||
## Site Settings
|
||||
- Much of Discourse is configured by site settings. These are defined in `config/site_settings.yml` or `config/settings.yml` files.
|
||||
- Site Setting functionality is defined in `lib/site_setting_extension.rb`
|
||||
- Site settings are accessed with `SiteSetting.setting_name` in ruby and `siteSettings.setting_name` in JS, with the latter needing a `@service siteSettings` declaration in Ember components
|
||||
- Configured in `config/site_settings.yml` or `config/settings.yml` for plugins
|
||||
- Functionality in `lib/site_setting_extension.rb`
|
||||
- Access: `SiteSetting.setting_name` (Ruby), `siteSettings.setting_name` (JS with `@service siteSettings`)
|
||||
|
||||
## Service objects
|
||||
- We have a service framework which is useful to extract business logic you usually find in controllers (validating parameters, fetching models, validating permissions, etc.). It’s not limited to controllers, though, and can be used anywhere.
|
||||
- This is documented at https://meta.discourse.org/t/using-service-objects-in-discourse/333641
|
||||
- Examples are found at `app/services` but ONLY for classes with include `Service::Base`
|
||||
## Services
|
||||
- Extract business logic (validation, models, permissions) from controllers
|
||||
- https://meta.discourse.org/t/using-service-objects-in-discourse/333641
|
||||
- Examples: `app/services` (only classes with `Service::Base`)
|
||||
|
||||
## Database & Performance
|
||||
- ActiveRecord: use `includes()`/`preload()` (N+1), `find_each()`/`in_batches()` (large sets), `update_all`/`delete_all` (bulk), `exists?` over `present?`
|
||||
- Migrations: rollback logic, `algorithm: :concurrently` for large tables, deprecate before removing columns
|
||||
- Queries: use `explain`, specify columns, strategic indexing, `counter_cache` for counts
|
||||
|
||||
### ActiveRecord Best Practices
|
||||
- Always use `includes()` or `preload()` to prevent N+1 queries when accessing associations
|
||||
- Use `find_each()` or `in_batches()` for large dataset processing
|
||||
- Prefer database-level operations (`update_all`, `delete_all`) over Ruby loops for bulk changes
|
||||
- Use `exists?` instead of `present?` when checking for record existence
|
||||
## Security
|
||||
- XSS: use `{{}}` (escaped) not `{{{ }}}`, sanitize with `sanitize`/`cook`, no `innerHTML`, careful with `@html`
|
||||
- Auth: Guardian classes (`lib/guardian.rb`), POST/PUT/DELETE for state changes, CSRF tokens, `protect_from_forgery`
|
||||
- Input: validate client+server, strong parameters, length limits, don't trust client-only validation
|
||||
- Authorization: Guardian classes, route+action permissions, scope limiting, `can_see?`/`can_edit?` patterns
|
||||
|
||||
### Migration Guidelines
|
||||
- Always include rollback logic in migrations
|
||||
- Use `add_index(..., algorithm: :concurrently)` for large tables in production
|
||||
- Never remove columns directly - deprecate first, then remove in subsequent release
|
||||
- Test migrations on production-sized datasets when possible
|
||||
|
||||
### Query Optimization
|
||||
- Use `explain` to analyze query performance during development
|
||||
- Avoid `SELECT *` - specify needed columns explicitly
|
||||
- Use database indexes strategically, but avoid over-indexing
|
||||
- Consider using `counter_cache` for frequently accessed counts
|
||||
|
||||
## Security Guidelines
|
||||
|
||||
### XSS Prevention
|
||||
- Always use `{{}}` (escaped) instead of `{{{ }}}` (unescaped) in Ember templates
|
||||
- Sanitize user input using Discourse's built-in helpers (`sanitize`, `cook`)
|
||||
- Never directly insert user content into `innerHTML` or similar DOM methods
|
||||
- Use `@html` argument carefully and only with pre-sanitized content
|
||||
|
||||
- Always use Guardian classes for authorization checks, the Guardian class defined in lib/guardian.rb
|
||||
- There are other Guardian classes defined in lib/guardian
|
||||
- All state-changing requests must use POST/PUT/DELETE, never GET
|
||||
- Ensure CSRF tokens are included in AJAX requests
|
||||
- Use Rails' `protect_from_forgery` in controllers handling sensitive operations
|
||||
|
||||
### Input Sanitization
|
||||
- Validate and sanitize all user inputs on both client and server side
|
||||
- Use strong parameters in Rails controllers
|
||||
- Apply appropriate length limits and format validation
|
||||
- Never trust client-side validation alone
|
||||
|
||||
### Authorization
|
||||
- Always use Guardian classes for authorization checks, the Guardian class defined in `lib/guardian.rb`
|
||||
- There are other Guardian classes defined in `lib/guardian`
|
||||
- Check permissions at both route and action levels
|
||||
- Implement proper scope limiting (users should only see their own data)
|
||||
- Use `can_see?` and `can_edit?` patterns consistently
|
||||
|
||||
## Knowledge Sharing and Persistence
|
||||
|
||||
- When asked to remember something, ALWAYS persist this information in a way that's accessible to ALL developers, not just in conversational memory
|
||||
- Document important information in appropriate files (comments, documentation, README, etc.) so other developers (human or AI) can access it
|
||||
- Information should be stored in a structured way that follows project conventions
|
||||
- NEVER keep crucial information only in conversational memory - this creates knowledge silos
|
||||
- If asked to implement something that won't be accessible to other users/developers in the repository, proactively highlight this issue
|
||||
- The goal is complete knowledge sharing between ALL developers (human and AI) without exceptions
|
||||
- When suggesting where to store information, recommend appropriate locations based on the type of information (code comments, documentation files, AI-AGENTS.md, etc.)
|
||||
- Inform the developer when you detect a change in this file and have successfully reloaded it.
|
||||
## Knowledge Sharing
|
||||
- ALWAYS persist information for ALL developers (no conversational-only memory)
|
||||
- Follow project conventions, prevent knowledge silos
|
||||
- Recommend storage locations by info type
|
||||
- Inform when this file changes and reloads
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
See @AI-AGENTS.md for all instructions.
|
1
CLAUDE.md
Symbolic link
1
CLAUDE.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
AI-AGENTS.md
|
1
GEMINI.md
Symbolic link
1
GEMINI.md
Symbolic link
|
@ -0,0 +1 @@
|
|||
AI-AGENTS.md
|
374
bin/lint
Executable file
374
bin/lint
Executable file
|
@ -0,0 +1,374 @@
|
|||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "optparse"
|
||||
require "json"
|
||||
require "open3"
|
||||
require "digest"
|
||||
|
||||
class Linter
|
||||
BATCH_SIZE = 5000
|
||||
|
||||
LINTERS = {
|
||||
rb: {
|
||||
rubocop: {
|
||||
check: "bundle exec rubocop --force-exclusion {files}",
|
||||
fix: "bundle exec rubocop --force-exclusion -A {files}",
|
||||
},
|
||||
syntax_tree: {
|
||||
check: "bundle exec stree check Gemfile {files}",
|
||||
fix: "bundle exec stree write Gemfile {files}",
|
||||
},
|
||||
},
|
||||
rake: {
|
||||
syntax_tree: {
|
||||
check: "bundle exec stree check {files}",
|
||||
fix: "bundle exec stree write {files}",
|
||||
},
|
||||
},
|
||||
js: {
|
||||
eslint: {
|
||||
check: "pnpm eslint --quiet {files}",
|
||||
fix: "pnpm eslint --fix {files}",
|
||||
},
|
||||
prettier: {
|
||||
check: "pnpm pprettier --list-different {files}",
|
||||
fix: "pnpm pprettier --write {files}",
|
||||
},
|
||||
},
|
||||
gjs: {
|
||||
eslint: {
|
||||
check: "pnpm eslint --quiet {files}",
|
||||
fix: "pnpm eslint --fix {files}",
|
||||
},
|
||||
prettier: {
|
||||
check: "pnpm pprettier --list-different {files}",
|
||||
fix: "pnpm pprettier --write {files}",
|
||||
},
|
||||
ember_template_lint: {
|
||||
check: "pnpm ember-template-lint {files}",
|
||||
fix: "pnpm ember-template-lint --fix {files}",
|
||||
},
|
||||
},
|
||||
hbs: {
|
||||
ember_template_lint: {
|
||||
check: "pnpm ember-template-lint {files}",
|
||||
fix: "pnpm ember-template-lint --fix {files}",
|
||||
},
|
||||
prettier: {
|
||||
check: "pnpm pprettier --list-different {files}",
|
||||
fix: "pnpm pprettier --write {files}",
|
||||
},
|
||||
},
|
||||
scss: {
|
||||
stylelint: {
|
||||
check: "pnpm stylelint {files}",
|
||||
fix: "pnpm stylelint --fix {files}",
|
||||
},
|
||||
prettier: {
|
||||
check: "pnpm pprettier --list-different {files}",
|
||||
fix: "pnpm pprettier --write {files}",
|
||||
},
|
||||
},
|
||||
css: {
|
||||
prettier: {
|
||||
check: "pnpm pprettier --list-different {files}",
|
||||
fix: "pnpm pprettier --write {files}",
|
||||
},
|
||||
},
|
||||
yml: {
|
||||
yaml_syntax: {
|
||||
check: "bundle exec yaml-lint {files}",
|
||||
},
|
||||
i18n_lint: {
|
||||
check: "bundle exec ruby script/i18n_lint.rb {files}",
|
||||
},
|
||||
},
|
||||
yaml: {
|
||||
yaml_syntax: {
|
||||
check: "bundle exec yaml-lint {files}",
|
||||
},
|
||||
},
|
||||
}.freeze
|
||||
|
||||
def initialize(options = {})
|
||||
@fix = options[:fix]
|
||||
@recent = options[:recent]
|
||||
@files = options[:files] || []
|
||||
@verbose = options[:verbose]
|
||||
@errors = []
|
||||
@fixed = []
|
||||
end
|
||||
|
||||
def run
|
||||
files_to_lint = determine_files
|
||||
return success_message if files_to_lint.empty?
|
||||
|
||||
# Phase 1: Group files by linter type and filter appropriately
|
||||
linter_files = group_files_by_linter(files_to_lint)
|
||||
|
||||
# Phase 2: Process each linter's files in batches
|
||||
linter_files.each do |linter_name, files|
|
||||
next if files.empty?
|
||||
|
||||
files.each_slice(BATCH_SIZE) { |file_batch| run_linter_batch(linter_name, file_batch) }
|
||||
end
|
||||
|
||||
print_summary
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def determine_files
|
||||
return recent_files if @recent
|
||||
return @files unless @files.empty?
|
||||
|
||||
all_lintable_files
|
||||
end
|
||||
|
||||
def recent_files
|
||||
log_output, status = Open3.capture2("git", "log", "-50", "--name-only", "--pretty=format:")
|
||||
return [] unless status.success?
|
||||
log_files = log_output.lines.map(&:strip).reject(&:empty?)
|
||||
|
||||
tracked_out, tracked_status = Open3.capture2("git", "ls-files")
|
||||
untracked_out, untracked_status =
|
||||
Open3.capture2("git", "ls-files", "--others", "--exclude-standard")
|
||||
return [] unless tracked_status.success? && untracked_status.success?
|
||||
|
||||
tracked = Set.new(tracked_out.lines.map(&:strip))
|
||||
untracked = Set.new(untracked_out.lines.map(&:strip))
|
||||
|
||||
# Only keep log files that are still tracked, then add all untracked
|
||||
candidates = Set.new
|
||||
log_files.each { |f| candidates << f if tracked.include?(f) }
|
||||
untracked.each { |f| candidates << f }
|
||||
|
||||
candidates.select { |f| lintable_file?(f) }
|
||||
end
|
||||
|
||||
def all_lintable_files
|
||||
tracked_out, tracked_status = Open3.capture2("git", "ls-files")
|
||||
untracked_out, untracked_status =
|
||||
Open3.capture2("git", "ls-files", "--others", "--exclude-standard")
|
||||
return [] unless tracked_status.success? && untracked_status.success?
|
||||
|
||||
tracked = tracked_out.lines.map(&:strip)
|
||||
untracked = untracked_out.lines.map(&:strip)
|
||||
|
||||
(tracked + untracked).select { |f| File.file?(f) && lintable_file?(f) }
|
||||
end
|
||||
|
||||
def lintable_file?(file)
|
||||
if file.include?("node_modules") || file.include?("vendor") || file.include?("tmp") ||
|
||||
file.include?(".git") || file == "database.yml"
|
||||
return false
|
||||
end
|
||||
|
||||
ext = File.extname(file)[1..]
|
||||
|
||||
# Check for Ruby files in /bin/ directory without extensions
|
||||
if ext.nil? || ext.empty?
|
||||
if file.start_with?("bin/") && File.file?(file)
|
||||
begin
|
||||
first_line = File.open(file, &:readline)
|
||||
return true if first_line.strip == "#!/usr/bin/env ruby"
|
||||
rescue StandardError
|
||||
# If we can't read the file, skip it
|
||||
return false
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
LINTERS.key?(ext.to_sym) || (file.include?("/assets/") && %w[js scss css hbs gjs].include?(ext))
|
||||
end
|
||||
|
||||
def group_files_by_extension(files)
|
||||
files
|
||||
.group_by do |f|
|
||||
ext = File.extname(f)[1..]
|
||||
# Treat Ruby files in /bin/ without extensions as Ruby files
|
||||
if (ext.nil? || ext.empty?) && f.start_with?("bin/") && File.file?(f)
|
||||
begin
|
||||
first_line = File.open(f, &:readline)
|
||||
ext = "rb" if first_line.strip == "#!/usr/bin/env ruby"
|
||||
rescue StandardError
|
||||
# If we can't read the file, keep original extension
|
||||
end
|
||||
end
|
||||
ext
|
||||
end
|
||||
.reject { |ext, _| ext.nil? || ext.empty? }
|
||||
end
|
||||
|
||||
def group_files_by_linter(files)
|
||||
linter_files = {}
|
||||
grouped_files = group_files_by_extension(files)
|
||||
|
||||
grouped_files.each do |ext, ext_files|
|
||||
linters = LINTERS[ext.to_sym]
|
||||
next unless linters
|
||||
|
||||
linters.each do |linter_name, _commands|
|
||||
filtered_files = filter_files_for_linter(linter_name, ext_files)
|
||||
next if filtered_files.empty?
|
||||
|
||||
linter_files[linter_name] ||= []
|
||||
linter_files[linter_name].concat(filtered_files)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove duplicates that might occur if a file matches multiple linters
|
||||
linter_files.each { |linter_name, file_list| linter_files[linter_name] = file_list.uniq }
|
||||
linter_files
|
||||
end
|
||||
|
||||
def run_linter_batch(linter_name, files)
|
||||
# Find the command for this linter by looking through all file types
|
||||
command = find_command_for_linter(linter_name)
|
||||
return unless command
|
||||
|
||||
# Split command into parts and replace {files} placeholder
|
||||
base_command = command.gsub("{files}", "").strip.split
|
||||
cmd_args = base_command + files
|
||||
|
||||
puts "Running #{linter_name} on #{files.size} files..." if @verbose
|
||||
|
||||
if @fix
|
||||
changed_files = detect_changed_files(files, cmd_args)
|
||||
@fixed.concat(changed_files)
|
||||
else
|
||||
output, status = Open3.capture2e(*cmd_args)
|
||||
|
||||
unless status.success?
|
||||
failed_files = parse_failed_files(linter_name, output, files)
|
||||
@errors << { linter: linter_name, files: failed_files, output: output }
|
||||
puts "#{linter_name} failed on: #{failed_files.join(", ")}" if @verbose
|
||||
puts output unless output.strip.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def find_command_for_linter(linter_name)
|
||||
LINTERS.each do |_ext, linters|
|
||||
linters.each do |name, commands|
|
||||
return @fix ? commands[:fix] : commands[:check] if name == linter_name
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def detect_changed_files(files, cmd_args)
|
||||
file_checksums = {}
|
||||
files.each { |file| file_checksums[file] = file_checksum(file) }
|
||||
|
||||
Open3.capture2e(*cmd_args)
|
||||
|
||||
changed_files = []
|
||||
files.each { |file| changed_files << file if file_checksum(file) != file_checksums[file] }
|
||||
|
||||
changed_files
|
||||
end
|
||||
|
||||
def file_checksum(file)
|
||||
return nil unless File.exist?(file)
|
||||
Digest::MD5.file(file).hexdigest
|
||||
end
|
||||
|
||||
def parse_failed_files(linter_name, output, all_files)
|
||||
case linter_name
|
||||
when :eslint
|
||||
output.scan(%r{^(/[^\s:]+):\d+:\d+}).flatten.uniq
|
||||
when :prettier
|
||||
lines = output.lines.map(&:strip)
|
||||
lines.select { |line| all_files.any? { |f| line.end_with?(File.basename(f)) } }
|
||||
when :rubocop
|
||||
output.scan(/^([^:]+):\d+:\d+/).flatten.uniq
|
||||
when :ember_template_lint
|
||||
output.scan(/^([^\s:]+):\d+:\d+/).flatten.uniq
|
||||
when :stylelint
|
||||
output.scan(/^([^\s:]+)\s+\d+:\d+/).flatten.uniq
|
||||
when :syntax_tree
|
||||
output.scan(/\[\d+m([^\[\]]+)\[0m/).flatten.uniq.select { |f| all_files.include?(f) }
|
||||
when :yaml_syntax, :i18n_lint
|
||||
all_files.select { |f| output.include?(f) }
|
||||
else
|
||||
all_files
|
||||
end
|
||||
end
|
||||
|
||||
def filter_files_for_linter(name, files)
|
||||
case name
|
||||
when :prettier, :ember_template_lint
|
||||
files.select do |file|
|
||||
file.include?("/assets/javascripts/") || file.include?("/plugins/") ||
|
||||
file.include?("/themes/")
|
||||
end
|
||||
when :i18n_lint
|
||||
files.select { |file| file.match?(%r{/(client|server)\..*\.yml$}) }
|
||||
when :yaml_syntax
|
||||
files.select { |file| !file.end_with?("database.yml") }
|
||||
when :syntax_tree
|
||||
# syntax_tree doesn't handle files without extensions well, skip for bin/ files
|
||||
files.select { |file| !file.start_with?("bin/") || File.extname(file) != "" }
|
||||
else
|
||||
files
|
||||
end
|
||||
end
|
||||
|
||||
def print_summary
|
||||
if @errors.empty?
|
||||
if @fix && @fixed.any?
|
||||
puts "✅ Fixed #{@fixed.size} files"
|
||||
else
|
||||
puts "✅ All files passed linting"
|
||||
end
|
||||
else
|
||||
puts "❌ Linting failed on #{@errors.map { |e| e[:files] }.flatten.uniq.size} files"
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def success_message
|
||||
puts "No files to lint"
|
||||
end
|
||||
end
|
||||
|
||||
def parse_options
|
||||
options = {}
|
||||
|
||||
OptionParser
|
||||
.new do |parser|
|
||||
parser.banner = "Usage: bin/lint [options] [files...]"
|
||||
|
||||
parser.on("-h", "--help", "Show this help message") do
|
||||
puts parser
|
||||
puts
|
||||
puts "Examples:"
|
||||
puts " bin/lint # Lint all files"
|
||||
puts " bin/lint --recent # Lint recently changed files"
|
||||
puts " bin/lint --fix app.rb # Fix specific file"
|
||||
puts " bin/lint app/models/*.rb # Lint multiple files"
|
||||
exit
|
||||
end
|
||||
|
||||
parser.on("-f", "--fix", "Attempt to automatically fix issues") { options[:fix] = true }
|
||||
|
||||
parser.on("-r", "--recent", "Lint recently changed files (last 50 commits)") do
|
||||
options[:recent] = true
|
||||
end
|
||||
|
||||
parser.on("-v", "--verbose", "Show verbose output") { options[:verbose] = true }
|
||||
end
|
||||
.parse!
|
||||
|
||||
options[:files] = ARGV unless ARGV.empty?
|
||||
options
|
||||
end
|
||||
|
||||
if __FILE__ == $0
|
||||
options = parse_options
|
||||
linter = Linter.new(options)
|
||||
linter.run
|
||||
end
|
|
@ -38,6 +38,8 @@ class LocaleFileValidator
|
|||
"The following keys use {{key}} instead of %{key} for interpolation keys:",
|
||||
wrong_pluralization_keys:
|
||||
"Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:",
|
||||
invalid_file_format:
|
||||
"The file is not a valid YAML format or does not contain a valid locale structure.",
|
||||
invalid_one_keys:
|
||||
"The following keys contain the number 1 instead of the interpolation key %{count}:",
|
||||
}.merge(
|
||||
|
@ -72,6 +74,9 @@ class LocaleFileValidator
|
|||
validate_content(yaml)
|
||||
|
||||
@errors.any? { |_, value| value.any? }
|
||||
rescue StandardError => e
|
||||
@errors[:invalid_file_format] = ["Failed to parse #{@filename}: #{e.message}"]
|
||||
true
|
||||
end
|
||||
|
||||
def print_errors
|
||||
|
@ -128,6 +133,10 @@ class LocaleFileValidator
|
|||
end
|
||||
|
||||
def validate_pluralizations(yaml)
|
||||
if !yaml.is_a?(Hash)
|
||||
@errors[:wrong_pluralization_keys] << ["Root of the locale file must be a hash"]
|
||||
return
|
||||
end
|
||||
each_pluralization(yaml) do |key, hash|
|
||||
# ignore errors from some ActiveRecord messages
|
||||
next if key.include?("messages.restrict_dependent_destroy")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue