Merge: serialization: fix a bug, improve doc and clean weird condition
authorJean Privat <jean@pryen.org>
Fri, 26 Aug 2016 21:09:29 +0000 (17:09 -0400)
committerJean Privat <jean@pryen.org>
Fri, 26 Aug 2016 21:09:29 +0000 (17:09 -0400)
Pull-Request: #2297
Reviewed-by: Jean Privat <jean@pryen.org>

19 files changed:
contrib/github_merge.nit
lib/core/README.md
lib/core/file.nit
lib/github/README.md
lib/json/serialization.nit
lib/nlp/README.md
lib/pipeline.nit
src/doc/doc_phases/doc_console.nit
src/doc/doc_phases/doc_readme.nit
src/model/model_index.nit
src/model/model_views.nit
src/modelize/modelize_property.nit
src/web/api_docdown.nit
tests/base_virtual_type4.nit
tests/sav/base_virtual_type4.res
tests/sav/base_virtual_type4_alt1.res [deleted file]
tests/sav/nitx_args1.res
tests/sav/nitx_args2.res
tests/sav/nitx_args3.res

index 52afa49..ca61f39 100644 (file)
@@ -17,6 +17,7 @@ module github_merge
 
 import github::github_curl
 import template
+import opts
 
 redef class Object
        # Factorize cast
@@ -27,12 +28,16 @@ end
 
 redef class GithubCurl
        # Get a given pull request (PR)
-       fun getpr(number: Int): JsonObject
+       fun getpr(repo: String, number: Int): nullable JsonObject
        do
-               var pr = get_and_check("https://api.github.com/repos/nitlang/nit/pulls/{number}")
+               var ir = get_and_check("https://api.github.com/repos/{repo}/issues/{number}")
+               var irm = ir.json_as_map
+               if not irm.has_key("pull_request") then return null
+               var pr = get_and_check("https://api.github.com/repos/{repo}/pulls/{number}")
                var prm = pr.json_as_map
                var sha = prm["head"].json_as_map["sha"].to_s
-               var statuses = get_and_check("https://api.github.com/repos/nitlang/nit/statuses/{sha}")
+               var statuses = get_and_check("https://api.github.com/repos/{repo}/commits/{sha}/status")
+               statuses = statuses.json_as_map
                prm["statuses"] = statuses
                print "{prm["title"].to_s}: by {prm["user"].json_as_map["login"].to_s} (# {prm["number"].to_s})"
                var mergeable = prm["mergeable"]
@@ -41,23 +46,31 @@ redef class GithubCurl
                else
                        print "\tmergeable: unknown"
                end
-               var st = prm["statuses"].json_as_a
-               if not st.is_empty then
-                       print "\tstatus: {st[0].json_as_map["state"].to_s}"
-               else
+               var state = statuses["state"]
+               if state == null then
                        print "\tstatus: not tested"
+               else
+                       print "\tstatus: {state}"
+                       var sts = statuses["statuses"].json_as_a
+                       for st in sts do
+                               st = st.json_as_map
+                               var ctx = st["context"].to_s
+                               state = st["state"].to_s
+                               print "\tstatus {ctx}: {state}"
+                               prm["status-{ctx}"] = state
+                       end
                end
                return prm
        end
 
        # Get reviewers of a PR
-       fun getrev(pr: JsonObject): Array[String]
+       fun getrev(repo: String, pr: JsonObject): Array[String]
        do
                var number = pr["number"].as(Int)
                var user = pr["user"].json_as_map["login"].as(String)
                var comments = new Array[nullable Object]
-               comments.add_all(get_and_check("https://api.github.com/repos/nitlang/nit/issues/{number}/comments").json_as_a)
-               comments.add_all(get_and_check("https://api.github.com/repos/nitlang/nit/pulls/{number}/comments").json_as_a)
+               comments.add_all(get_and_check("https://api.github.com/repos/{repo}/issues/{number}/comments").json_as_a)
+               comments.add_all(get_and_check("https://api.github.com/repos/{repo}/pulls/{number}/comments").json_as_a)
                var logins = new Array[String]
                for c in comments do
                        var cm = c.json_as_map
@@ -83,27 +96,61 @@ end
 
 if "NIT_TESTING".environ == "true" then exit 0
 
-var auth = get_github_oauth
+var opt_repo = new OptionString("Repository (e.g. nitlang/nit)", "-r", "--repo")
+var opt_auth = new OptionString("OAuth token", "--auth")
+var opt_query = new OptionString("Query to get issues (e.g. label=ok_will_merge)", "-q", "--query")
+var opt_keepgoing = new OptionBool("Skip merge conflicts", "-k", "--keep-going")
+var opt_all = new OptionBool("Merge all", "-a", "--all")
+var opt_status = new OptionArray("A status context that must be \"success\" (e.g. default)", "--status")
+var opts = new OptionContext
+opts.add_option(opt_repo, opt_auth, opt_query, opt_status, opt_all, opt_keepgoing)
+
+opts.parse(sys.args)
+var args = opts.rest
 
+var auth = opt_auth.value or else ""
+if auth == "" then auth = get_github_oauth
 if auth == "" then
        print "Warning: no github oauth token, you can configure one with"
        print "    git config --add github.oauthtoken MYOAUTHTOKEN"
 end
 
+var repo = opt_repo.value or else "nitlang/nit"
+
+var query = opt_query.value or else "labels=ok_will_merge"
+
 var curl = new GithubCurl(auth, "Merge-o-matic (nitlang/nit)")
 
-if args.length != 1 then
+if args.is_empty then
        # Without args, list `ok_will_merge`
-       var x = curl.get_and_check("https://api.github.com/repos/nitlang/nit/issues?labels=ok_will_merge")
+       var x = curl.get_and_check("https://api.github.com/repos/{repo}/issues?{query}")
+       var list = new Array[String]
        for y in x.json_as_a do
                var number = y.json_as_map["number"].as(Int)
-               curl.getpr(number)
-       end
-else
+               var pr = curl.getpr(repo, number)
+               if pr == null then continue
+               for ctx in opt_status.value do
+                       if pr.get_or_null("status-{ctx}") != "success" then
+                               print "No \"success\" for {ctx}. Skip."
+                               continue label
+                       end
+               end
+               list.add number.to_s
+       end label
+
+       if not opt_all.value then return
+       args = list
+end
+
+for arg in args do
        # With a arg, merge the PR
-       var number = args.first.to_i
-       var pr = curl.getpr(number)
-       var revs = curl.getrev(pr)
+       var number = arg.to_i
+       var pr = curl.getpr(repo, number)
+       if pr == null then
+               print "Not a PR: {number}"
+               return
+       end
+       var revs = curl.getrev(repo, pr)
 
        var mergemsg = new Template
        mergemsg.add "Merge: {pr["title"].to_s}\n\n"
@@ -119,7 +166,15 @@ else
                print "Commit {sha} not in local repository; did you fetch github?"
                return
        end
+       if system("git merge-base --is-ancestor {sha} HEAD") == 0 then
+               print "Is already merged."
+               continue
+       end
        if system("git merge --no-ff --no-commit {sha}") != 0 then
+               if opt_keepgoing.value then
+                        system("git reset --merge")
+                        continue
+               end
                system("cp mergemsg `git rev-parse --git-dir`/MERGE_MSG")
                print "Problem during merge... Let's do the commit manually."
                return
index 7833f37..f394594 100644 (file)
@@ -4,43 +4,43 @@ Core classes and methods used by default by Nit programs and libraries.
 
 ## Core Basic Types and Operations
 
-[[doc:kernel]]
+[[doc: kernel]]
 
 ### Object
 
-[[doc:Object]]
+[[doc: Object]]
 
 #### Equality
 
-[[doc:Object::==]]
-[[doc:Object::!=]]
-[[doc:Object::hash]]
-[[doc:Object::is_same_instance]]
-[[doc:Object::object_id]]
+[[doc: core::Object::==]]
+[[doc: core::Object::!=]]
+[[doc: core::Object::hash]]
+[[doc: core::Object::is_same_instance]]
+[[doc: core::Object::object_id]]
 
 #### Debuging
 
-[[doc:Object::output]]
-[[doc:Object::output_class_name]]
-[[doc:Object::is_same_type]]
+[[doc: core::Object::output]]
+[[doc: core::Object::output_class_name]]
+[[doc: core::Object::is_same_type]]
 
 ### Sys
 
-[[doc:Sys]]
+[[doc: Sys]]
 
 #### Program Execution
 
-[[doc:Sys::main]]
-[[doc:Sys::run]]
+[[doc: core::Sys::main]]
+[[doc: core::Sys::run]]
 
 ### Other
 
-[[list:kernel]]
+[[list: kernel]]
 
 ## Core Collections
 
-[[doc:collection]]
+[[doc: collection]]
 
 ## String and Text manipulation
 
-[[doc:text]]
+[[doc: text]]
index 23d3ce3..dc1ccb7 100644 (file)
@@ -888,12 +888,12 @@ redef class Text
        do
                for i in substrings do s.write_native(i.to_cstring, 0, i.byte_length)
        end
-end
 
-redef class String
        # return true if a file with this names exists
        fun file_exists: Bool do return to_cstring.file_exists
+end
 
+redef class String
        # The status of a file. see POSIX stat(2).
        fun file_stat: nullable FileStat
        do
@@ -1216,15 +1216,21 @@ redef class String
                        path.add('/')
                end
                var error: nullable Error = null
-               for d in dirs do
+               for i in [0 .. dirs.length - 1[ do
+                       var d = dirs[i]
                        if d.is_empty then continue
                        path.append(d)
                        path.add('/')
-                       var res = path.to_s.to_cstring.file_mkdir(mode)
+                       if path.file_exists then continue
+                       var res = path.to_cstring.file_mkdir(mode)
                        if not res and error == null then
                                error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
                        end
                end
+               var res = self.to_cstring.file_mkdir(mode)
+               if not res and error == null then
+                       error = new IOError("Cannot create directory `{path}`: {sys.errno.strerror}")
+               end
                return error
        end
 
index 64a7e98..041e753 100644 (file)
@@ -8,7 +8,7 @@ This module provides a Nit object oriented interface to access the Github api.
 
 ### Authentification
 
-[[doc: GithubAPI::auth]]
+[[doc: auth]]
 
 Token can also be recovered from user config with `get_github_oauth`.
 
@@ -28,7 +28,7 @@ Token can also be recovered from user config with `get_github_oauth`.
 
 ### Other data
 
-[[list: api]]
+[[list: github::api]]
 
 ### Advanced uses
 
@@ -38,11 +38,11 @@ Token can also be recovered from user config with `get_github_oauth`.
 
 #### Custom requests
 
-[[doc: GithubAPI::get]]
+[[doc: github::GithubAPI::get]]
 
 #### Change the user agent
 
-[[doc: GithubAPI::user_agent]]
+[[doc: github::GithubAPI::user_agent]]
 
 #### Debugging
 
index 8861e19..e26b8ec 100644 (file)
@@ -290,7 +290,14 @@ class JsonDeserializer
 
        redef fun deserialize_attribute(name, static_type)
        do
-               assert not path.is_empty # This is an internal error, abort
+               if path.is_empty then
+                       # The was a parsing error or the root is not an object
+                       if not root isa Error then
+                               errors.add new Error("Deserialization Error: parsed JSON value is not an object.")
+                       end
+                       return null
+               end
+
                var current = path.last
 
                if not current.keys.has(name) then
index 6545537..c7196c4 100644 (file)
@@ -33,41 +33,41 @@ For ease of use, this wrapper introduce a Nit model to handle CoreNLP XML result
 
 [[doc: NLPDocument]]
 
-[[doc: NLPDocument::from_xml]]
-[[doc: NLPDocument::from_xml_file]]
-[[doc: NLPDocument::sentences]]
+[[doc: nlp::NLPDocument::from_xml]]
+[[doc: nlp::NLPDocument::from_xml_file]]
+[[doc: nlp::NLPDocument::sentences]]
 
 ### NLPSentence
 
 [[doc: NLPSentence]]
 
-[[doc: NLPSentence::tokens]]
+[[doc: nlp::NLPSentence::tokens]]
 
 ### NLPToken
 
 [[doc: NLPToken]]
 
-[[doc: NLPToken::word]]
-[[doc: NLPToken::lemma]]
-[[doc: NLPToken::pos]]
+[[doc: nlp::NLPToken::word]]
+[[doc: nlp::NLPToken::lemma]]
+[[doc: nlp::NLPToken::pos]]
 
 ### NLP Processor
 
 [[doc: NLPProcessor]]
 
-[[doc: NLPProcessor::java_cp]]
+[[doc: nlp::NLPProcessor::java_cp]]
 
-[[doc: NLPProcessor::process]]
-[[doc: NLPProcessor::process_file]]
-[[doc: NLPProcessor::process_files]]
+[[doc: nlp::NLPProcessor::process]]
+[[doc: nlp::NLPProcessor::process_file]]
+[[doc: nlp::NLPProcessor::process_files]]
 
 ## Vector Space Model
 
 [[doc: NLPVector]]
 
-[[doc: NLPDocument::vector]]
+[[doc: vector]]
 
-[[doc: NLPVector::cosine_similarity]]
+[[doc: nlp::NLPVector::cosine_similarity]]
 
 ## NitNLP binary
 
index d3428f0..54be22b 100644 (file)
@@ -73,6 +73,8 @@ redef interface Iterator[E]
        # When the first iterator is terminated, the second is started.
        #
        #     assert ([1..20[.iterator + [20..40[.iterator).to_a             ==  ([1..40[).to_a
+       #
+       # SEE: `Iterator2`
        fun +(other: Iterator[E]): Iterator[E]
        do
                return new PipeJoin[E](self, other)
@@ -159,6 +161,95 @@ redef interface Iterator[E]
        end
 end
 
+# Concatenates a sequence of iterators.
+#
+# Wraps an iterator of sub-iterators and iterates over the elements of the
+# sub-iterators.
+#
+# ~~~nit
+# var i: Iterator[Int]
+# var empty = new Array[Int]
+#
+# i = new Iterator2[Int]([
+#      [1, 2, 3].iterator,
+#      empty.iterator,
+#      [4, 5].iterator
+# ].iterator)
+# assert i.to_a == [1, 2, 3, 4, 5]
+#
+# i = new Iterator2[Int]([
+#      empty.iterator,
+#      [42].iterator,
+#      empty.iterator
+# ].iterator)
+# assert i.to_a == [42]
+# ~~~
+#
+# SEE: `Iterator::+`
+class Iterator2[E]
+       super Iterator[E]
+
+       # The inner iterator over sub-iterators.
+       var inner: Iterator[Iterator[E]]
+
+       redef fun finish
+       do
+               var i = current_iterator
+               if i != null then i.finish
+       end
+
+       redef fun is_ok
+       do
+               var i = current_iterator
+               if i == null then return false
+               return i.is_ok
+       end
+
+       redef fun item
+       do
+               var i = current_iterator
+               assert i != null
+               return i.item
+       end
+
+       redef fun next
+       do
+               var i = current_iterator
+               assert i != null
+               i.next
+       end
+
+       redef fun start
+       do
+               var i = current_iterator
+               if i != null then i.start
+       end
+
+       private var previous_iterator: nullable Iterator[E] = null
+
+       private fun current_iterator: nullable Iterator[E]
+       do
+               if previous_iterator == null then
+                       # Get the first sub-iterator.
+                       if inner.is_ok then
+                               previous_iterator = inner.item
+                               previous_iterator.start
+                               inner.next
+                       else
+                               return null
+                       end
+               end
+               # Get the first sub-iterator that has a current item.
+               while inner.is_ok and not previous_iterator.is_ok do
+                       previous_iterator.finish
+                       previous_iterator = inner.item
+                       previous_iterator.start
+                       inner.next
+               end
+               return previous_iterator
+       end
+end
+
 # Wraps an iterator to skip nulls.
 #
 # ~~~nit
index 56fbc74..e8cb281 100644 (file)
@@ -22,6 +22,7 @@ import semantize
 import doc_commands
 import doc_poset
 import doc::console_templates
+import model::model_index
 
 # Nitx handles console I/O.
 #
@@ -95,7 +96,11 @@ class Nitx
                        return
                end
                var res = query.perform(self, doc)
-               var page = query.make_results(self, res)
+               var suggest = null
+               if res.is_empty then
+                       suggest = query.suggest(self, doc)
+               end
+               var page = query.make_results(self, res, suggest)
                print page.write_to_string
        end
 end
@@ -118,12 +123,44 @@ redef interface DocCommand
        # Looks up the `doc` model and returns possible matches.
        fun perform(nitx: Nitx, doc: DocModel): Array[NitxMatch] is abstract
 
+       # Looks up the `doc` model and returns possible suggestions.
+       fun suggest(nitx: Nitx, doc: DocModel): nullable Array[MEntity] do
+               return find_suggestions(doc, args.first)
+       end
+
        # Pretty prints the results for the console.
-       fun make_results(nitx: Nitx, results: Array[NitxMatch]): DocPage do
+       fun make_results(nitx: Nitx, results: Array[NitxMatch], suggest: nullable Array[MEntity]): DocPage do
                var page = new DocPage("results", "Results")
-               page.root.add_child(new QueryResultArticle("results", "Results", self, results))
+               page.root.add_child(new QueryResultArticle("results", "Results", self, results, suggest))
                return page
        end
+
+       # Lookup mentities based on a `query` string.
+       #
+       # 1- lookup by first name (returns always one value)
+       # 2- lookup by name (can return conflicts)
+       fun find_mentities(doc: DocModel, query: String): Array[MEntityMatch] do
+               var res = new Array[MEntityMatch]
+
+               # First lookup by full_name
+               var mentity = doc.mentity_by_full_name(query)
+               if mentity != null then
+                       res.add new MEntityMatch(self, mentity)
+                       return res
+               end
+
+               # If no results, lookup by name
+               for m in doc.mentities_by_name(query) do
+                       res.add new MEntityMatch(self, m)
+               end
+
+               return res
+       end
+
+       # Suggest some mentities based on a `query` string.
+       fun find_suggestions(doc: DocModel, query: String): Array[MEntity] do
+               return doc.find(query, 3)
+       end
 end
 
 # Something that matches a `DocCommand`.
@@ -147,16 +184,9 @@ class MEntityMatch
 end
 
 redef class CommentCommand
-       redef fun perform(nitx, doc) do
-               var name = args.first
-               var res = new Array[NitxMatch]
-               for mentity in doc.mentities_by_name(name) do
-                       res.add new MEntityMatch(self, mentity)
-               end
-               return res
-       end
+       redef fun perform(nitx, doc) do return find_mentities(doc, args.first)
 
-       redef fun make_results(nitx, results) do
+       redef fun make_results(nitx, results, suggest) do
                var len = results.length
                if len == 1 then
                        var res = results.first.as(MEntityMatch)
@@ -266,7 +296,7 @@ redef class ArticleCommand
                return res
        end
 
-       redef fun make_results(nitx, results) do
+       redef fun make_results(nitx, results, suggest) do
                var len = results.length
                # FIXME how to render the pager for one worded namespaces like "core"?
                if len == 1 then
@@ -304,7 +334,7 @@ end
 abstract class HierarchiesQuery
        super DocCommand
 
-       redef fun make_results(nitx, results) do
+       redef fun make_results(nitx, results, suggest) do
                var page = new DocPage("hierarchy", "Hierarchy")
                for result in results do
                        if not result isa PageMatch then continue
@@ -382,15 +412,15 @@ redef class CodeCommand
                        return res
                end
                # else, lookup the model by name
-               for mentity in doc.mentities_by_name(name) do
-                       if mentity isa MClass then continue
-                       if mentity isa MProperty then continue
-                       res.add new CodeMatch(self, mentity.cs_location, mentity.cs_source_code)
+               for match in find_mentities(doc, name) do
+                       if match.mentity isa MClass then continue
+                       if match.mentity isa MProperty then continue
+                       res.add new CodeMatch(self, match.mentity.cs_location, match.mentity.cs_source_code)
                end
                return res
        end
 
-       redef fun make_results(nitx, results) do
+       redef fun make_results(nitx, results, suggest) do
                var page = new DocPage("results", "Code Results")
                for res in results do
                        page.add new CodeQueryArticle("results", "Results", self, res.as(CodeMatch))
@@ -487,10 +517,25 @@ private class QueryResultArticle
        # Results to display.
        var results: Array[NitxMatch]
 
+       # Optional suggestion when no matches where found
+       var suggest: nullable Array[MEntity] = null is optional
+
        redef fun render_title do
                var len = results.length
                if len == 0 then
-                       add "No result found for '{query.string}'..."
+                       addn "No result found for '{query.string}'..."
+                       var suggest = self.suggest
+                       if suggest != null and suggest.not_empty then
+                               add "\nDid you mean "
+                               var i = 0
+                               for s in suggest do
+                                       add "`{s.full_name}`"
+                                       if i == suggest.length - 2 then add ", "
+                                       if i == suggest.length - 1 then add " or "
+                                       i += 1
+                               end
+                               add "?"
+                       end
                else
                        add "# {len} result(s) for '{query.string}'".green.bold
                end
index b34fc7f..c026b77 100644 (file)
@@ -20,6 +20,7 @@ intrude import markdown::wikilinks
 import doc_commands
 import doc_down
 import doc_intros_redefs
+import model::model_index
 
 # Generate content of `ReadmePage`.
 #
@@ -146,6 +147,37 @@ class ReadmeMdEmitter
                context.pop
                pop_buffer
        end
+
+       # Find mentities matching `query`.
+       fun find_mentities(query: String): Array[MEntity] do
+               # search MEntities by full_name
+               var mentity = phase.doc.mentity_by_full_name(query)
+               if mentity != null then return [mentity]
+               # search MEntities by name
+               return phase.doc.mentities_by_name(query)
+       end
+
+       # Suggest mentities based on `query`.
+       fun suggest_mentities(query: String): Array[MEntity] do
+               return phase.doc.find(query, 3)
+       end
+
+       # Display a warning message with suggestions.
+       fun warn(token: TokenWikiLink, message: String, suggest: nullable Array[MEntity]) do
+               var msg = new Buffer
+               msg.append message
+               if suggest != null and suggest.not_empty then
+                       msg.append " (suggestions: "
+                       var i = 0
+                       for s in suggest do
+                               msg.append "`{s.full_name}`"
+                               if i < suggest.length - 1 then msg.append ", "
+                               i += 1
+                       end
+                       msg.append ")"
+               end
+               phase.warning(token.location, page, msg.write_to_string)
+       end
 end
 
 # MarkdownDecorator used to decorated the Readme file with links between doc entities.
@@ -173,10 +205,10 @@ class ReadmeDecorator
                var cmd = new DocCommand(link)
                if cmd isa UnknownCommand then
                        # search MEntities by name
-                       var res = v.phase.doc.mentities_by_name(link.to_s)
+                       var res = v.find_mentities(link.to_s)
                        # no match, print warning and display wikilink as is
                        if res.is_empty then
-                               v.phase.warning(token.location, v.page, "Link to unknown entity `{link}`")
+                               v.warn(token, "Link to unknown entity `{link}`", v.suggest_mentities(link.to_s))
                                super
                        else
                                add_mentity_link(v, res.first, token.name, token.comment)
@@ -202,57 +234,34 @@ redef interface DocCommand
 
        # Render the content of the doc command.
        fun render(v: ReadmeMdEmitter, token: TokenWikiLink) is abstract
-
-       # Search `doc` model for mentities match `string`.
-       fun search_model(doc: DocModel, string: String): Array[MEntity] do
-               var res
-               if string.has("::") then
-                       res = doc.mentities_by_namespace(string).to_a
-               else
-                       res = doc.mentities_by_name(string).to_a
-               end
-               return res
-       end
 end
 
 redef class ArticleCommand
        redef fun render(v, token) do
                var string = args.first
-               var res = search_model(v.phase.doc, string)
-               res = filter_results(res)
+               var res = v.find_mentities(string)
                if res.is_empty then
-                       v.phase.warning(
-                               token.location, v.page,
-                               "Try to include documentation of unknown entity `{args.first}`")
+                       v.warn(token,
+                               "Try to include documentation of unknown entity `{string}`",
+                               v.suggest_mentities(string))
                        return
                end
-               if res.length > 1 then
-                       v.phase.warning(token.location, v.page, "conflicting article for `{args.first}` (choices : {res.join(", ")})")
-               end
                v.add_article new DocumentationArticle("readme", "Readme", res.first)
        end
-
-       private fun filter_results(res: Array[MEntity]): Array[MEntity] do
-               var out = new Array[MEntity]
-               for e in res do
-                       if e isa MPackage then continue
-                       if e isa MGroup then continue
-                       out.add e
-               end
-               return out
-       end
 end
 
 redef class ListCommand
        redef fun render(v, token) do
                var string = args.first
-               var res = search_model(v.phase.doc, string)
+               var res = v.find_mentities(string)
                if res.is_empty then
-                       v.phase.warning(token.location, v.page, "include article for unknown entity `{args.first}`")
+                       v.warn(token,
+                               "Try to include article of unknown entity `{string}`",
+                               v.suggest_mentities(string))
                        return
                end
                if res.length > 1 then
-                       v.phase.warning(token.location, v.page, "conflicting article for `{args.first}` (choices : {res.join(", ")})")
+                       v.warn(token, "Conflicting article for `{args.first}`", res)
                end
                var mentity = res.first
                if mentity isa MModule then
index e84e922..4136243 100644 (file)
@@ -130,6 +130,98 @@ module model_index
 import model::model_views
 import trees::trie
 
+redef class ModelView
+
+       # Keep a direct link to mentities by full name to speed up `mentity_from_uri`
+       var mentities_by_full_name: HashMap[String, MEntity] is lazy do
+               var mentities_by_full_name = new HashMap[String, MEntity]
+               for mentity in model.private_view.mentities do
+                       mentities_by_full_name[mentity.full_name] = mentity
+               end
+               return mentities_by_full_name
+       end
+
+       # ModelIndex used to perform searches
+       var index: ModelIndex is lazy do
+               var index = new ModelIndex
+               for mentity in model.private_view.mentities do
+                       if mentity isa MClassDef or mentity isa MPropDef then continue
+                       index.index mentity
+               end
+               return index
+       end
+
+       # Find mentities by their `name`
+       fun mentities_by_name(name: String): Array[MEntity] do
+               if index.name_prefixes.has_key(name) then
+                       return index.name_prefixes[name]
+               end
+               return new Array[MEntity]
+       end
+
+       redef fun mentity_by_full_name(full_name) do
+               if mentities_by_full_name.has_key(full_name) then
+                       return mentities_by_full_name[full_name]
+               end
+               return null
+       end
+
+       private var score_sorter = new ScoreComparator
+       private var vis_sorter = new VisibilityComparator
+       private var kind_sorter = new MEntityComparator
+       private var name_sorter = new NameComparator
+       private var lname_sorter = new NameLengthComparator
+       private var fname_sorter = new FullNameComparator
+       private var lfname_sorter = new FullNameLengthComparator
+
+       # Search mentities based on a `query` string
+       #
+       # Lookup the view index for anything matching `query` and return `limit` results.
+       #
+       # The algorithm used is the following:
+       # 1- lookup by name prefix
+       # 2- lookup by full_name prefix
+       # 3- loopup by levenshtein distance
+       #
+       # At each step if the `limit` is reached, the algorithm stops and returns the results.
+       fun find(query: String, limit: nullable Int): Array[MEntity] do
+               # Find, lookup by name prefix
+               var matches = index.find_by_name_prefix(query).uniq.
+                       sort(lname_sorter, name_sorter, kind_sorter)
+               if limit != null and matches.length >= limit then
+                       return matches.limit(limit).rerank.sort(vis_sorter, score_sorter).mentities
+               end
+               matches = matches.rerank.sort(vis_sorter, score_sorter)
+
+               # If limit not reached, lookup by full_name prefix
+               var malus = matches.length
+               var full_matches = new IndexMatches
+               for match in index.find_by_full_name_prefix(query).
+                       sort(kind_sorter, lfname_sorter, fname_sorter) do
+                       match.score += malus
+                       full_matches.add match
+               end
+               matches = matches.uniq
+               if limit != null and matches.length + full_matches.length >= limit then
+                       matches.add_all full_matches
+                       matches = matches.uniq.limit(limit).rerank.sort(vis_sorter, score_sorter)
+                       return matches.mentities
+               end
+
+               # If limit not reached, lookup by similarity
+               malus = matches.length
+               var sim_matches = new IndexMatches
+               for match in index.find_by_similarity(query).sort(score_sorter, kind_sorter, lname_sorter, name_sorter) do
+                       match.score += malus
+                       sim_matches.add match
+               end
+               matches.add_all sim_matches
+               matches = matches.uniq
+               if limit != null then matches = matches.limit(limit)
+               return matches.rerank.sort(vis_sorter, score_sorter).mentities
+       end
+end
+
 # ModelIndex indexes mentities by their name and full name
 #
 # It provides methods to find mentities based on a prefix or string similarity.
index 566a5bd..2eec952 100644 (file)
@@ -125,13 +125,6 @@ class ModelView
                v.include_test_suite = self.include_test_suite
        end
 
-       # Searches MEntities that match `name`.
-       fun mentities_by_name(name: String): Array[MEntity] do
-               var res = new Array[MEntity]
-               for mentity in mentities do if mentity.name == name then res.add mentity
-               return res
-       end
-
        # Searches the MEntity that matches `full_name`.
        fun mentity_by_full_name(full_name: String): nullable MEntity do
                for mentity in mentities do
@@ -140,20 +133,6 @@ class ModelView
                return null
        end
 
-       # Looks up a MEntity by its full `namespace`.
-       #
-       # Usefull when `mentities_by_name` returns conflicts.
-       #
-       # Namespaces must be of the form `package::core::module::Class::prop`.
-       fun mentities_by_namespace(namespace: String): Array[MEntity] do
-               var v = new LookupNamespaceVisitor(namespace)
-               init_visitor(v)
-               for mpackage in mpackages do
-                       v.enter_visit(mpackage)
-               end
-               return v.results
-       end
-
        # Build an concerns tree with from `self`
        fun to_tree: MEntityTree do
                var v = new ModelTreeVisitor
index a313ab6..9465dbb 100644 (file)
@@ -1685,8 +1685,7 @@ redef class ATypePropdef
                                break
                        end
                        if p.mclassdef.mclass == mclassdef.mclass then
-                               # Still a warning to pass existing bad code
-                               modelbuilder.warning(n_type, "refine-type", "Redef Error: a virtual type cannot be refined.")
+                               modelbuilder.error(n_type, "Redef Error: a virtual type cannot be refined.")
                                break
                        end
                        if not modelbuilder.check_subtype(n_type, mmodule, anchor, bound, supbound) then
index 778baa1..02d6b22 100644 (file)
@@ -19,6 +19,7 @@ import api_graph
 intrude import doc_down
 intrude import markdown::wikilinks
 import doc_commands
+import model::model_index
 
 redef class APIRouter
        redef init do
@@ -88,10 +89,49 @@ redef interface DocCommand
                write_error(v, "Not yet implemented command `{token.link or else "null"}`")
        end
 
-       # Find the MEntity ` with `full_name`.
-       fun find_mentity(model: ModelView, full_name: nullable String): nullable MEntity do
-               if full_name == null then return null
-               return model.mentity_by_full_name(full_name.from_percent_encoding)
+       # Find the MEntity that matches `name`.
+       #
+       # Write an error if the entity is not found
+       fun find_mentity(v: MarkdownEmitter, model: ModelView, name: nullable String): nullable MEntity do
+               if name == null then
+                       write_error(v, "No MEntity found")
+                       return null
+               end
+               # Lookup by full name
+               var mentity = model.mentity_by_full_name(name)
+               if mentity != null then return mentity
+
+               var mentities = model.mentities_by_name(name)
+               if mentities.is_empty then
+                       var suggest = model.find(name, 3)
+                       var msg = new Buffer
+                       msg.append "No MEntity found for name `{name}`"
+                       if suggest.not_empty then
+                               msg.append " (suggestions: "
+                               var i = 0
+                               for s in suggest do
+                                       msg.append "`{s.full_name}`"
+                                       if i < suggest.length - 1 then msg.append ", "
+                                       i += 1
+                               end
+                               msg.append ")"
+                       end
+                       write_error(v, msg.write_to_string)
+                       return null
+               else if mentities.length > 1 then
+                       var msg = new Buffer
+                       msg.append "Conflicts for name `{name}`"
+                       msg.append " (conflicts: "
+                       var i = 0
+                       for s in mentities do
+                               msg.append "`{s.full_name}`"
+                               if i < mentities.length - 1 then msg.append ", "
+                               i += 1
+                       end
+                       msg.append ")"
+                       write_warning(v, msg.write_to_string)
+               end
+               return mentities.first
        end
 
        # Write a warning in the output
@@ -123,11 +163,8 @@ redef class UnknownCommand
                        return
                end
                var full_name = link.write_to_string
-               var mentity = find_mentity(model, full_name)
-               if mentity == null then
-                       write_error(v, "Unknown command `{link}`")
-                       return
-               end
+               var mentity = find_mentity(v, model, full_name)
+               if mentity == null then return
                write_mentity_link(v, mentity)
        end
 end
@@ -139,11 +176,8 @@ redef class ArticleCommand
                        return
                end
                var name = args.first
-               var mentity = find_mentity(model, name)
-               if mentity == null then
-                       write_error(v, "No MEntity found for name `{name}`")
-                       return
-               end
+               var mentity = find_mentity(v, model, name)
+               if mentity == null then return
                var mdoc = mentity.mdoc_or_fallback
                if mdoc == null then
                        write_warning(v, "No MDoc for mentity `{name}`")
@@ -165,11 +199,8 @@ redef class CommentCommand
                        return
                end
                var name = args.first
-               var mentity = find_mentity(model, name)
-               if mentity == null then
-                       write_error(v, "No MEntity found for name `{name}`")
-                       return
-               end
+               var mentity = find_mentity(v, model, name)
+               if mentity == null then return
                var mdoc = mentity.mdoc_or_fallback
                if mdoc == null then
                        write_warning(v, "No MDoc for mentity `{name}`")
@@ -186,7 +217,8 @@ redef class ListCommand
                        return
                end
                var name = args.first
-               var mentity = find_mentity(model, name)
+               var mentity = find_mentity(v, model, name)
+               if mentity == null then return
                if mentity isa MPackage then
                        write_list(v, mentity.mgroups)
                else if mentity isa MGroup then
@@ -231,11 +263,8 @@ redef class CodeCommand
                        return
                end
                var name = args.first
-               var mentity = find_mentity(model, name)
-               if mentity == null then
-                       write_error(v, "No MEntity found for name `{name}`")
-                       return
-               end
+               var mentity = find_mentity(v, model, name)
+               if mentity == null then return
                if mentity isa MClass then mentity = mentity.intro
                if mentity isa MProperty then mentity = mentity.intro
                var source = render_source(mentity, v.decorator.as(NitwebDecorator).modelbuilder)
@@ -265,11 +294,8 @@ redef class GraphCommand
                        return
                end
                var name = args.first
-               var mentity = find_mentity(model, name)
-               if mentity == null then
-                       write_error(v, "No MEntity found for name `{name}`")
-                       return
-               end
+               var mentity = find_mentity(v, model, name)
+               if mentity == null then return
                var g = new InheritanceGraph(mentity, model)
                v.add g.draw(3, 3).to_svg
        end
index 133d9ee..3f360dc 100644 (file)
@@ -21,6 +21,5 @@ redef class A
 end
 
 var c = new B
-#alt1# c.e = new T
 c.e = new U
 c.e.foo
index 13dcd34..b4034f1 100644 (file)
@@ -1,2 +1 @@
 base_virtual_type4.nit:20,16: Redef Error: a virtual type cannot be refined.
-1
diff --git a/tests/sav/base_virtual_type4_alt1.res b/tests/sav/base_virtual_type4_alt1.res
deleted file mode 100644 (file)
index cae0e93..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-alt/base_virtual_type4_alt1.nit:20,16: Redef Error: a virtual type cannot be refined.
-alt/base_virtual_type4_alt1.nit:24,7--11: Type Error: expected `nullable U`, got `T`.
index f22bfee..d9ab058 100644 (file)
@@ -1,13 +1,9 @@
 
-\e[1m\e[32m# 2 result(s) for 'comment: A'\e[m\e[m
+\e[1m\e[34m# \e[m\e[m\e[1m\e[34mA\e[m\e[m
+\e[1m\e[30mclass A\e[m\e[m
+
+    \e[1m\e[32mclass A\e[m\e[m
+    \e[1m\e[30mbase_simple3.nit:29,1--32,3\e[m\e[m
 
\e[1m\e[32mC\e[m\e[m \e[1m\e[34mA\e[m\e[m
-   \e[1m\e[30mbase_simple3::A\e[m\e[m
-   class A
-   \e[30mbase_simple3.nit:29,1--32,3\e[m
 
\e[1m\e[32mC\e[m\e[m \e[1m\e[34mA\e[m\e[m
-   \e[1m\e[30mbase_simple3::base_simple3::A\e[m\e[m
-   class A
-   \e[30mbase_simple3.nit:29,1--32,3\e[m
 
index b7ca320..3f6b1f1 100644 (file)
@@ -1,13 +1,9 @@
 
-\e[1m\e[32m# 2 result(s) for 'comment: foo'\e[m\e[m
+\e[1m\e[34m# \e[m\e[m\e[1m\e[34mfoo\e[m\e[m
+\e[1m\e[30mfun foo\e[m\e[m
+
+    \e[1m\e[32mfun foo\e[m\e[m
+    \e[1m\e[30mbase_simple3.nit:49,1--19\e[m\e[m
 
\e[1m\e[32mF\e[m\e[m \e[1m\e[34mfoo\e[m\e[m
-   \e[1m\e[30mbase_simple3::Sys::foo\e[m\e[m
-   fun foo
-   \e[30mbase_simple3.nit:49,1--19\e[m
 
\e[1m\e[32mF\e[m\e[m \e[1m\e[34mfoo\e[m\e[m
-   \e[1m\e[30mbase_simple3::base_simple3::Sys::foo\e[m\e[m
-   fun foo
-   \e[30mbase_simple3.nit:49,1--19\e[m
 
index 6bf30a6..9d71d91 100644 (file)
@@ -1,18 +1,9 @@
 
-\e[1m\e[32m# 3 result(s) for 'comment: base_simple3'\e[m\e[m
+\e[1m\e[34m# \e[m\e[m\e[1m\e[34mbase_simple3\e[m\e[m
+\e[1m\e[30mpackage base_simple3\e[m\e[m
 
\e[1m\e[32mP\e[m\e[m \e[1m\e[34mbase_simple3\e[m\e[m
-   \e[1m\e[30mbase_simple3\e[m\e[m
-   package base_simple3
-   \e[30mbase_simple3.nit:17,1--66,13\e[m
+    \e[1m\e[32mpackage base_simple3\e[m\e[m
+    \e[1m\e[30mbase_simple3.nit:17,1--66,13\e[m\e[m
 
\e[1m\e[32mG\e[m\e[m \e[1m\e[34mbase_simple3\e[m\e[m
-   \e[1m\e[30mbase_simple3\e[m\e[m
-   group base_simple3
-   \e[30mbase_simple3.nit:17,1--66,13\e[m
 
\e[1m\e[32mM\e[m\e[m \e[1m\e[34mbase_simple3\e[m\e[m
-   \e[1m\e[30mbase_simple3::base_simple3\e[m\e[m
-   module base_simple3
-   \e[30mbase_simple3.nit:17,1--66,13\e[m