{"id":1376,"date":"2026-06-18T11:51:20","date_gmt":"2026-06-18T18:51:20","guid":{"rendered":"https:\/\/markalldritt.com\/?p=1376"},"modified":"2026-06-18T11:51:23","modified_gmt":"2026-06-18T18:51:23","slug":"aelint","status":"publish","type":"post","link":"https:\/\/markalldritt.com\/?p=1376","title":{"rendered":"AELint"},"content":{"rendered":"<h1>Announcing aelint<\/h1>\n\n<p>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&#8217; scripting interfaces \u2014 and watching those interfaces let them down. Script Debugger&#8217;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&#8217;t actually reach.<\/p>\n\n<p><code>aelint<\/code> validates and tests a scriptable application&#8217;s scripting interface. It reads the same SDEF that my <code>aequery<\/code> tool uses, runs a set of static checks against the dictionary, and \u2014 if you ask it to \u2014 exercises the running application with live Apple Events to confirm the interface behaves the way it&#8217;s described.<\/p>\n\n<h2>What it checks<\/h2>\n\n<p>The static checks read the dictionary alone:<\/p>\n\n<ul>\n<li>Undefined classes, property types, and command parameter and result types<\/li>\n<li>Inheritance cycles and classes that can&#8217;t be reached from the application root<\/li>\n<li>Duplicate four-character codes across classes, commands, and parameters<\/li>\n<li>Missing or non-standard plural element names<\/li>\n<li>Reserved-word and Standard Suite term clashes<\/li>\n<li>Empty classes, unused enumerations, and missing documentation<\/li>\n<\/ul>\n\n<p>Each finding explains the consequence, because the point isn&#8217;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 <code>window of x<\/code> ambiguous, and the object specifier may end up pointing at the wrong thing.<\/p>\n\n<h2>Testing against the live app<\/h2>\n\n<p>Static analysis only goes so far. A dictionary can be perfectly well-formed and still describe behaviour the application doesn&#8217;t actually implement. With <code>--dynamic<\/code>, aelint connects to the running application and works through the interface a phase at a time \u2014 reading and counting elements, reaching them by index, ordinal, range, and <code>whose<\/code> clause, checking <code>exists<\/code>, validating that runtime types match what&#8217;s declared, and probing commands to confirm the app really handles the events its dictionary advertises.<\/p>\n\n<p>It&#8217;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 <code>make<\/code> and <code>delete<\/code>, it sticks to documents, windows, and tabs \u2014 it creates one, confirms the count went up, deletes it, and confirms the count returns to where it started. Destructive verbs like <code>quit<\/code>, <code>close<\/code>, <code>save<\/code>, and <code>duplicate<\/code> are never invoked.<\/p>\n\n<p>That said, these are real Apple Events going to a live application, and a bug in the app&#8217;s own handlers could still lose data. Run it against an application you don&#8217;t mind poking at, not one holding unsaved work.<\/p>\n\n<h2>Trying it<\/h2>\n\n<p>aelint installs from the same Homebrew tap as aequery:<\/p>\n\n<pre><code>brew tap alldritt\/tools\nbrew install aelint\n<\/code><\/pre>\n\n<p>Point it at an application by name:<\/p>\n\n<pre><code>$ aelint TextEdit\naelint report for TextEdit\n========================================\nBundle ID: com.apple.TextEdit\nVersion: 1.20\nClasses: 12, Commands: 13, Enumerations: 2\nFindings: 0 errors, 1 warnings, 3 info\n\n! WARNING (1)\n----------------------------------------\nundefined-class: Class 'attachment' inherits from undefined class 'text.ctxt'\n\ni INFO (3)\n----------------------------------------\nundefined-type: Property 'text' in class 'document' has type 'text.ctxt' which is not defined in the SDEF\nmissing-plural: Class 'attachment' has no explicit plural (defaults to 'attachments')\ndocumentation: 1 of 12 classes (8%) have no description\n\n========================================\nQuality Score: 99\/100 (A)\n========================================\n<\/code><\/pre>\n\n<p>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 <code>--json<\/code> and <code>--html<\/code>.<\/p>\n\n<h2>Feedback<\/h2>\n\n<p>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 \u2014 or misses something it should catch \u2014 I&#8217;d like to hear about it.<\/p>\n\n<p>The source and full documentation are on GitHub:<\/p>\n\n<p><a href=\"https:\/\/github.com\/alldritt\/aequery\">https:\/\/github.com\/alldritt\/aequery<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p><code>aelint<\/code> validates and tests a scriptable application&#8217;s scripting interface.<\/p><div class=\"more-link-wrapper\"><a class=\"more-link\" href=\"https:\/\/markalldritt.com\/?p=1376\">Continue Reading<span class=\"screen-reader-text\">AELint<\/span><\/a><\/div>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false}}},"categories":[6,9],"tags":[],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/s7AQk-aelint","_links":{"self":[{"href":"https:\/\/markalldritt.com\/index.php?rest_route=\/wp\/v2\/posts\/1376"}],"collection":[{"href":"https:\/\/markalldritt.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/markalldritt.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/markalldritt.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/markalldritt.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1376"}],"version-history":[{"count":4,"href":"https:\/\/markalldritt.com\/index.php?rest_route=\/wp\/v2\/posts\/1376\/revisions"}],"predecessor-version":[{"id":1380,"href":"https:\/\/markalldritt.com\/index.php?rest_route=\/wp\/v2\/posts\/1376\/revisions\/1380"}],"wp:attachment":[{"href":"https:\/\/markalldritt.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1376"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/markalldritt.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1376"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/markalldritt.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1376"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}