oocdoc, Part 4 — sourcepath
👋 This page was last updated ~12 years ago. Just so you know.
In the previous article, We've built a nagaqueen-based tool that can parse one ooc file, detect class declarations and print its doc strings. Today, we're making a bit of infrastructure for our app to support more sizable projects.
Source path and lib folders
Parsing a single file was a nice milestone, but it's not nearly enough. We want to generate documentation for a whole project at a time: and since we'll want to cross-link the various bits of documentation we generate, we'll also need to parse the various dependencies (such as the ooc sdk, and any used library) so that we can resolve argument types and link them properly.
To undertake that task, we need to agree on a little bit of vocabulary. The
thing we want to document, we'll call a project. Each folder that contains
a hierarchy of folders and .ooc source files, we'll call a libFolder. The
pathy part of an import
directive, we'll call a spec.
A list of libFolders will be called the sourcePath, similar to the Java
classpath - that's simply where we look for stuff that's imported. Normally,
the source path is set from .use files, and the SDK is found using environment
variables and/or the location of the rock
binary on your filesystem. But,
again, that's for later.
To handle the source path we'll create.. a SourcePath
class! Let's put it
into source/hopemage/sourcepath.ooc
:
SourcePath: class { libFolders := ArrayList<LibFolder> new() add: func (libPath: String) { libFolders add(LibFolder new(libPath)) } locate: func (spec: String) -> File { for (libFolder in libFolders) { file := File new(libFolder path, spec + ".ooc") if (file exists?()) { return file } } null } split: func (path: String) -> (LibFolder, String) { path = File new(path) getAbsolutePath() for (libFolder in libFolders) { if (path startsWith?(libFolder path)) { return (libFolder, libFolder toSpec(path)) } } (null, null) } }
Figuring out which imports are needed are left as an exercise to the reader. (Of course, it's easy to cheat)
As you can see, locate
takes a spec and tries to find it in our list
of libFolders. That'll be useful when dealing with imports, later.
As for split
, as its name indicates, it splits a full
path into a libFolder
and a spec
. This is useful in our current
scenario where we'll get a whole list of paths and we'll have to
figure out what their libFolders and specs are.
Split uses an ooc feature known as multi-return. It works almost like a tuple, although it's restricted to that certain scenario. We can call it several ways, for example, those all valid:
(libFolder, spec) := sourcePath split(path) (libFolder, _) := sourcePath split(path) libFolder := sourcePath split(path) (_, spec) := sourcePath split(path)
An underscore stands for "ignore this value". Put your most important return values first, so that people can disregard the others without using tuple syntax, as shown on line 3 of the example above.
Let's make a class for libFolders too, in the same module:
LibFolder: class { path: String modules := ArrayList<Module> new() init: func (libPath: String) { path = File new(libPath) getAbsolutePath() } add: func (module: Module) { module libFolder = this modules add(module) } contains?: func (module: Module) -> Bool { modules contains?(module) } toSpec: func (path: String) -> String { path substring(this path size) trimLeft(File separator) } }
Nothing too surprising here. If you didn't know, you can have a question mark or an exclamation mark at the end of your ooc functions, which is nice for those who return booleans and those who are destructive.
Now you're thinking with projects
We'll also need one additional class to contain all the information about our
project, and we'll name it Project
, in source/hopemage/project.oo
:
Project: class { sourcePath: SourcePath mainFolder: LibFolder init: func (=sourcePath) { if (sourcePath libFolders empty?()) { raise("SourcePath is empty, bailing out!") } mainFolder = sourcePath libFolders first() parseFolder(mainFolder) } parseFolder: func (libFolder: LibFolder) { File new(libFolder path) walk(|f| if (f path endsWith?(".ooc")) { parse(f path) } true ) } parse: func (path: String) { Frontend new(sourcePath, path) } }
The interesting part here is the parseFolder
method, which walks a whole
folder to find .ooc file, and parses all of them. We simply assume the first
folder in the source path is the main one - the one we're generating the
documentation of in the first place.
We've modified Frontend
a bit to work with SourcePath:
Frontend: class extends OocListener { module: Module sourcePath: SourcePath init: func (=sourcePath, path: String) { (libFolder, spec) := sourcePath split(path) module = Module new(libFolder, spec) parse(path) } // other methods (callbacks, etc.) }
We're also calling parse from Frontend init
init now.
As you can see, the constructor from Module has changed a bit:
we want it to know to which libFolder it belongs to.
Module: class { libFolder: LibFolder types := ArrayList<Type> new() spec: String init: func (=libFolder, =spec) { libFolder add(this) } }
Again, there are a few imports you'll need to add.
Playing with our new toys
Now that we have all the infrastructure to handle source paths, lib folders
and projects correctly, let's revamp our main class. Instaed of specifying
an ooc file to parse, we'll accept a --sourcepath=blah
argument for our
program.
Here's what Homa.ooc
looks like now:
Homa: class { versionString := "0.1" sourcePath := SourcePath new() init: func handle: func (args: ArrayList<String>) { parseArgs(args) if (sourcePath libFolders empty?()) { usage() exit(0) } else { parse() } } parseArgs: func (args: ArrayList<String>) { args removeAt(0) for (arg in args) { tokens := arg split('=') if (tokens size != 2) { onInvalidArg(arg) continue } match (tokens[0]) { case "--sourcepath" => sourcePath add(tokens[1]) case => onInvalidArg(arg) } } } onInvalidArg: func (arg: String) { "Invalid argument: %s, ignoring.." printfln(arg) } usage: func { "homa v%s" printfln(versionString) "Usage: homa --sourcepath=FOLDERS" println() } parse: func { project := Project new(sourcePath) for (module in project mainFolder modules) { "## %s" printfln(module spec) for (type in module types) { "### %s\n\n```%s```\n\n" printfln(type name, type doc raw) } } } }
ooc's match
works like a switch, more powerful, but less crazy than,
say, Scala's match. We have a way to complain about invalid arguments
but it doesn't crash our program.
String split
isn't in the default imports, so you'll have to
import text/StringTokenizer
to have it.
You can now run hopemage against itself, with homa --sourcefolder=source
.
For additional fun points, run hopemage's output against a markdown tool
and open it in your browser: it looks already doc-y!
That's it for this time! I hope you enjoy this series, please tell me if I went over some things too quickly, I'll gladly include additional information in these articles.
Here's another article just for you:
Learning Rust is... an experience. An emotional journey. I've rarely been more frustrated than in my first few months of trying to learn Rust.
What makes it worse is that it doesn't matter how much prior experience you have, in Java, C#, C or C++ or otherwise - it'll still be unnerving.
In fact, more experience probably makes it worse! The habits have settled in deeper, and there's a certain expectation that, by now, you should be able to get that done in a shorter amount of time.
will