/*
*   $Id: ruby.c 571 2007-06-24 23:32:14Z elliotth $
*
*   Copyright (c) 2000-2001, Thaddeus Covert <sahuagin@mediaone.net>
*   Copyright (c) 2002 Matthias Veit <matthias_veit@yahoo.de>
*   Copyright (c) 2004 Elliott Hughes <enh@acm.org>
*
*   This source code is released for free distribution under the terms of the
*   GNU General Public License.
*
*   This module contains functions for generating tags for Ruby language
*   files.
*/

/*
*   INCLUDE FILES
*/
#include "general.h"  /* must always come first */

#include <string.h>

#include "entry.h"
#include "parse.h"
#include "read.h"
#include "vstring.h"

/*
*   DATA DECLARATIONS
*/
typedef enum {
	K_UNDEFINED = -1, K_CLASS, K_METHOD, K_MODULE, K_SINGLETON
} rubyKind;

/*
*   DATA DEFINITIONS
*/
static kindOption RubyKinds [] = {
	{ TRUE, 'c', "class",  "classes" },
	{ TRUE, 'f', "method", "methods" },
	{ TRUE, 'm', "module", "modules" },
	{ TRUE, 'F', "singleton method", "singleton methods" }
};

static stringList* nesting = 0;

/*
*   FUNCTION DEFINITIONS
*/

/*
* Returns a string describing the scope in 'list'.
* We record the current scope as a list of entered scopes.
* Scopes corresponding to 'if' statements and the like are
* represented by empty strings. Scopes corresponding to
* modules and classes are represented by the name of the
* module or class.
*/
static vString* stringListToScope (const stringList* list)
{
	unsigned int i;
	unsigned int chunks_output = 0;
	vString* result = vStringNew ();
	const unsigned int max = stringListCount (list);
	for (i = 0; i < max; ++i)
	{
	    vString* chunk = stringListItem (list, i);
	    if (vStringLength (chunk) > 0)
	    {
	        vStringCatS (result, (chunks_output++ > 0) ? "." : "");
	        vStringCatS (result, vStringValue (chunk));
	    }
	}
	return result;
}

/*
* Attempts to advance 's' past 'literal'.
* Returns TRUE if it did, FALSE (and leaves 's' where
* it was) otherwise.
*/
static boolean canMatch (const unsigned char** s, const char* literal)
{
	const int literal_length = strlen (literal);
	const unsigned char next_char = *(*s + literal_length);
	if (strncmp ((const char*) *s, literal, literal_length) != 0)
	{
	    return FALSE;
	}
	/* Additionally check that we're at the end of a token. */
	if ( ! (next_char == 0 || isspace (next_char) || next_char == '('))
	{
	    return FALSE;
	}
	*s += literal_length;
	return TRUE;
}

/*
* Attempts to advance 'cp' past a Ruby operator method name. Returns
* TRUE if successful (and copies the name into 'name'), FALSE otherwise.
*/
static boolean parseRubyOperator (vString* name, const unsigned char** cp)
{
	static const char* RUBY_OPERATORS[] = {
	    "[]", "[]=",
	    "**",
	    "!", "~", "+@", "-@",
	    "*", "/", "%",
	    "+", "-",
	    ">>", "<<",
	    "&",
	    "^", "|",
	    "<=", "<", ">", ">=",
	    "<=>", "==", "===", "!=", "=~", "!~",
	    "`",
	    0
	};
	int i;
	for (i = 0; RUBY_OPERATORS[i] != 0; ++i)
	{
	    if (canMatch (cp, RUBY_OPERATORS[i]))
	    {
	        vStringCatS (name, RUBY_OPERATORS[i]);
	        return TRUE;
	    }
	}
	return FALSE;
}

/*
* Emits a tag for the given 'name' of kind 'kind' at the current nesting.
*/
static void emitRubyTag (vString* name, rubyKind kind)
{
	tagEntryInfo tag;
	vString* scope;

	vStringTerminate (name);
	scope = stringListToScope (nesting);

	initTagEntry (&tag, vStringValue (name));
	if (vStringLength (scope) > 0) {
	    tag.extensionFields.scope [0] = "class";
	    tag.extensionFields.scope [1] = vStringValue (scope);
	}
	tag.kindName = RubyKinds [kind].name;
	tag.kind = RubyKinds [kind].letter;
	makeTagEntry (&tag);

	stringListAdd (nesting, vStringNewCopy (name));

	vStringClear (name);
	vStringDelete (scope);
}

/* Tests whether 'ch' is a character in 'list'. */
static boolean charIsIn (char ch, const char* list)
{
	return (strchr (list, ch) != 0);
}

/* Advances 'cp' over leading whitespace. */
static void skipWhitespace (const unsigned char** cp)
{
	while (isspace (**cp))
	{
	    ++*cp;
	}
}

/*
* Copies the characters forming an identifier from *cp into
* name, leaving *cp pointing to the character after the identifier.
*/
static rubyKind parseIdentifier (
		const unsigned char** cp, vString* name, rubyKind kind)
{
	/* Method names are slightly different to class and variable names.
	 * A method name may optionally end with a question mark, exclamation
	 * point or equals sign. These are all part of the name.
	 * A method name may also contain a period if it's a singleton method.
	 */
	const char* also_ok = (kind == K_METHOD) ? "_.?!=" : "_";

	skipWhitespace (cp);

	/* Check for an anonymous (singleton) class such as "class << HTTP". */
	if (kind == K_CLASS && **cp == '<' && *(*cp + 1) == '<')
	{
		return K_UNDEFINED;
	}

	/* Check for operators such as "def []=(key, val)". */
	if (kind == K_METHOD || kind == K_SINGLETON)
	{
		if (parseRubyOperator (name, cp))
		{
			return kind;
		}
	}

	/* Copy the identifier into 'name'. */
	while (**cp != 0 && (isalnum (**cp) || charIsIn (**cp, also_ok)))
	{
		char last_char = **cp;

		vStringPut (name, last_char);
		++*cp;

		if (kind == K_METHOD)
		{
			/* Recognize singleton methods. */
			if (last_char == '.')
			{
				vStringTerminate (name);
				vStringClear (name);
				return parseIdentifier (cp, name, K_SINGLETON);
			}

			/* Recognize characters which mark the end of a method name. */
			if (charIsIn (last_char, "?!="))
			{
				break;
			}
		}
	}
	return kind;
}

static void readAndEmitTag (const unsigned char** cp, rubyKind expected_kind)
{
	if (isspace (**cp))
	{
		vString *name = vStringNew ();
		rubyKind actual_kind = parseIdentifier (cp, name, expected_kind);

		if (actual_kind == K_UNDEFINED || vStringLength (name) == 0)
		{
			/*
			* What kind of tags should we create for code like this?
			*
			*    %w(self.clfloor clfloor).each do |name|
			*        module_eval <<-"end;"
			*            def #{name}(x, y=1)
			*                q, r = x.divmod(y)
			*                q = q.to_i
			*                return q, r
			*            end
			*        end;
			*    end
			*
			* Or this?
			*
			*    class << HTTP
			*
			* For now, we don't create any.
			*/
		}
		else
		{
			emitRubyTag (name, actual_kind);
		}
		vStringDelete (name);
	}
}

static void enterUnnamedScope (void)
{
	stringListAdd (nesting, vStringNewInit (""));
}

static void findRubyTags (void)
{
	const unsigned char *line;
	boolean inMultiLineComment = FALSE;

	nesting = stringListNew ();

	/* FIXME: this whole scheme is wrong, because Ruby isn't line-based.
	* You could perfectly well write:
	*
	*  def
	*  method
	*   puts("hello")
	*  end
	*
	* if you wished, and this function would fail to recognize anything.
	*/
	while ((line = fileReadLine ()) != NULL)
	{
		const unsigned char *cp = line;

		if (canMatch (&cp, "=begin"))
		{
			inMultiLineComment = TRUE;
			continue;
		}
		if (canMatch (&cp, "=end"))
		{
			inMultiLineComment = FALSE;
			continue;
		}

		skipWhitespace (&cp);

		/* Avoid mistakenly starting a scope for modifiers such as
		*
		*   return if <exp>
		*
		* FIXME: this is fooled by code such as
		*
		*   result = if <exp>
		*               <a>
		*            else
		*               <b>
		*            end
		*
		* FIXME: we're also fooled if someone does something heinous such as
		*
		*   puts("hello") \
		*       unless <exp>
		*/
		if (canMatch (&cp, "case") || canMatch (&cp, "for") ||
			canMatch (&cp, "if") || canMatch (&cp, "unless") ||
			canMatch (&cp, "while"))
		{
			enterUnnamedScope ();
		}

		/*
		* "module M", "class C" and "def m" should only be at the beginning
		* of a line.
		*/
		if (canMatch (&cp, "module"))
		{
			readAndEmitTag (&cp, K_MODULE);
		}
		else if (canMatch (&cp, "class"))
		{
			readAndEmitTag (&cp, K_CLASS);
		}
		else if (canMatch (&cp, "def"))
		{
			readAndEmitTag (&cp, K_METHOD);
		}

		while (*cp != '\0')
		{
			/* FIXME: we don't cope with here documents,
			* or regular expression literals, or ... you get the idea.
			* Hopefully, the restriction above that insists on seeing
			* definitions at the starts of lines should keep us out of
			* mischief.
			*/
			if (inMultiLineComment || isspace (*cp))
			{
				++cp;
			}
			else if (*cp == '#')
			{
				/* FIXME: this is wrong, but there *probably* won't be a
				* definition after an interpolated string (where # doesn't
				* mean 'comment').
				*/
				break;
			}
			else if (canMatch (&cp, "begin") || canMatch (&cp, "do"))
			{
				enterUnnamedScope ();
			}
			else if (canMatch (&cp, "end") && stringListCount (nesting) > 0)
			{
				/* Leave the most recent scope. */
				vStringDelete (stringListLast (nesting));
				stringListRemoveLast (nesting);
			}
			else if (*cp == '"')
			{
				/* Skip string literals.
				 * FIXME: should cope with escapes and interpolation.
				 */
				do {
					++cp;
				} while (*cp != 0 && *cp != '"');
			}
			else if (*cp != '\0')
			{
				do
					++cp;
				while (isalnum (*cp) || *cp == '_');
			}
		}
	}
	stringListDelete (nesting);
}

extern parserDefinition* RubyParser (void)
{
	static const char *const extensions [] = { "rb", "ruby", NULL };
	parserDefinition* def = parserNew ("Ruby");
	def->kinds      = RubyKinds;
	def->kindCount  = KIND_COUNT (RubyKinds);
	def->extensions = extensions;
	def->parser     = findRubyTags;
	return def;
}

/* vi:set tabstop=4 shiftwidth=4: */
