Announcing aelint
For thirty years I built and maintained Script Debugger, an AppleScript IDE. A good part of that work was helping people make sense of other applications’ scripting interfaces — and watching those interfaces let them down. Script Debugger’s dictionary viewer surfaced the same defects over and over: terms that collide, four-character codes that are duplicated, classes you can name but can’t actually reach.
aelint validates and tests a scriptable application’s scripting interface. It reads the same SDEF that my aequery tool uses, runs a set of static checks against the dictionary, and — if you ask it to — exercises the running application with live Apple Events to confirm the interface behaves the way it’s described.
What it checks
The static checks read the dictionary alone:
- Undefined classes, property types, and command parameter and result types
- Inheritance cycles and classes that can’t be reached from the application root
- Duplicate four-character codes across classes, commands, and parameters
- Missing or non-standard plural element names
- Reserved-word and Standard Suite term clashes
- Empty classes, unused enumerations, and missing documentation
Each finding explains the consequence, because the point isn’t to count style nits. A duplicated four-character code is a good example: AppleScript identifies everything by its code, not its name, so two terms sharing a code makes the dictionary ambiguous and a script can resolve to the wrong object. An element and a property that share a name make window of x ambiguous, and the object specifier may end up pointing at the wrong thing.
Testing against the live app
Static analysis only goes so far. A dictionary can be perfectly well-formed and still describe behaviour the application doesn’t actually implement. With --dynamic, aelint connects to the running application and works through the interface a phase at a time — reading and counting elements, reaching them by index, ordinal, range, and whose clause, checking exists, validating that runtime types match what’s declared, and probing commands to confirm the app really handles the events its dictionary advertises.
It’s careful about this. The tests are designed to be non-destructive. When it checks that a property is settable, it reads the current value and writes the same value straight back. When it exercises make and delete, it sticks to documents, windows, and tabs — it creates one, confirms the count went up, deletes it, and confirms the count returns to where it started. Destructive verbs like quit, close, save, and duplicate are never invoked.
That said, these are real Apple Events going to a live application, and a bug in the app’s own handlers could still lose data. Run it against an application you don’t mind poking at, not one holding unsaved work.
Trying it
aelint installs from the same Homebrew tap as aequery:
brew tap alldritt/tools
brew install aelint
Point it at an application by name:
$ aelint TextEdit
aelint report for TextEdit
========================================
Bundle ID: com.apple.TextEdit
Version: 1.20
Classes: 12, Commands: 13, Enumerations: 2
Findings: 0 errors, 1 warnings, 3 info
! WARNING (1)
----------------------------------------
undefined-class: Class 'attachment' inherits from undefined class 'text.ctxt'
i INFO (3)
----------------------------------------
undefined-type: Property 'text' in class 'document' has type 'text.ctxt' which is not defined in the SDEF
missing-plural: Class 'attachment' has no explicit plural (defaults to 'attachments')
documentation: 1 of 12 classes (8%) have no description
========================================
Quality Score: 99/100 (A)
========================================
It exits with a non-zero status when it finds errors, so you can drop it into a build to keep a dictionary honest as it changes. JSON and HTML reports are available with --json and --html.
Feedback
aelint is new, and I expect the checks will grow as it meets more dictionaries in the wild. If you run it against your own application and it gets something wrong — or misses something it should catch — I’d like to hear about it.
The source and full documentation are on GitHub: