com.google.javascript.jscomp.GatherModuleMetadata Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of closure-compiler-unshaded Show documentation
Show all versions of closure-compiler-unshaded Show documentation
Closure Compiler is a JavaScript optimizing compiler. It parses your
JavaScript, analyzes it, removes dead code and rewrites and minimizes
what's left. It also checks syntax, variable references, and types, and
warns about common JavaScript pitfalls. It is used in many of Google's
JavaScript apps, including Gmail, Google Web Search, Google Maps, and
Google Docs.
The newest version!
/*
* Copyright 2018 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.javascript.jscomp.ClosureCheckModule.DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE;
import static com.google.javascript.jscomp.ClosurePrimitiveErrors.INVALID_REQUIRE_NAMESPACE;
import com.google.common.base.Splitter;
import com.google.common.collect.LinkedHashMultiset;
import com.google.javascript.jscomp.deps.ModuleLoader.ModulePath;
import com.google.javascript.jscomp.deps.ModuleLoader.ResolutionMode;
import com.google.javascript.jscomp.modules.ModuleMetadataMap;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleMetadata;
import com.google.javascript.jscomp.modules.ModuleMetadataMap.ModuleType;
import com.google.javascript.jscomp.parsing.parser.Identifiers;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.QualifiedName;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import org.jspecify.nullness.Nullable;
/**
* Gathers metadata around modules that is useful for checking imports / requires and creates a
* {@link ModuleMetadataMap}.
*/
public final class GatherModuleMetadata implements CompilerPass {
static final DiagnosticType MIXED_MODULE_TYPE =
DiagnosticType.error("JSC_MIXED_MODULE_TYPE", "A file cannot be both {0} and {1}.");
static final DiagnosticType INVALID_NAMESPACE_OR_MODULE_ID =
DiagnosticType.error(
"JSC_INVALID_NAMESPACE_OR_MODULE_ID",
"Namespace and module ID must be a dot-separated sequence of legal property"
+ " identifiers and must only contain ASCII, 0-9, $, ., and _. Found ''{0}''");
static final DiagnosticType INVALID_DECLARE_MODULE_ID_CALL =
DiagnosticType.error(
"JSC_INVALID_DECLARE_NAMESPACE_CALL",
"goog.declareModuleId parameter must be a string literal.");
static final DiagnosticType DECLARE_MODULE_ID_OUTSIDE_ES6_MODULE =
DiagnosticType.error(
"JSC_DECLARE_MODULE_NAMESPACE_OUTSIDE_ES6_MODULE",
"goog.declareModuleId can only be called within ES6 modules.");
static final DiagnosticType MULTIPLE_DECLARE_MODULE_NAMESPACE =
DiagnosticType.error(
"JSC_MULTIPLE_DECLARE_MODULE_NAMESPACE",
"goog.declareModuleId can only be called once per ES6 module.");
static final DiagnosticType INVALID_REQUIRE_TYPE =
DiagnosticType.error(
"JSC_INVALID_REQUIRE_TYPE", "Argument to goog.requireType must be a string.");
static final DiagnosticType INVALID_REQUIRE_DYNAMIC =
DiagnosticType.error(
"JSC_INVALID_REQUIRE_DYNAMIC", "Argument to goog.requireDynamic must be a string.");
static final DiagnosticType INVALID_MAYBE_REQUIRE =
DiagnosticType.error(
"JSC_INVALID_MAYBE_REQUIRE",
"Argument to goog.maybeRequireFrameworkInternalOnlyDoNotCallOrElse must be a string.");
static final DiagnosticType INVALID_SET_TEST_ONLY =
DiagnosticType.error(
"JSC_INVALID_SET_TEST_ONLY",
"Optional, single argument to goog.setTestOnly must be a string.");
static final DiagnosticType INVALID_NESTED_LOAD_MODULE =
DiagnosticType.error("JSC_INVALID_NESTED_LOAD_MODULE", "goog.loadModule cannot be nested.");
static final DiagnosticType INVALID_TOGGLE_USAGE =
DiagnosticType.error("JSC_INVALID_TOGGLE_USAGE", "Invalid toggle usage: {0}");
private static final Node GOOG_PROVIDE = IR.getprop(IR.name("goog"), "provide");
private static final Node GOOG_MODULE = IR.getprop(IR.name("goog"), "module");
private static final Node GOOG_MODULE_GET =
IR.getprop(IR.getprop(IR.name("goog"), "module"), "get");
private static final Node GOOG_REQUIRE = IR.getprop(IR.name("goog"), "require");
private static final Node GOOG_REQUIRE_TYPE = IR.getprop(IR.name("goog"), "requireType");
private static final Node GOOG_MAYBE_REQUIRE =
IR.getprop(IR.name("goog"), "maybeRequireFrameworkInternalOnlyDoNotCallOrElse");
private static final Node GOOG_REQUIRE_DYNAMIC = IR.getprop(IR.name("goog"), "requireDynamic");
private static final Node GOOG_SET_TEST_ONLY = IR.getprop(IR.name("goog"), "setTestOnly");
private static final Node GOOG_MODULE_DECLARELEGACYNAMESPACE =
IR.getprop(GOOG_MODULE.cloneTree(), "declareLegacyNamespace");
private static final Node GOOG_DECLARE_MODULE_ID = IR.getprop(IR.name("goog"), "declareModuleId");
// TODO(johnplaisted): Remove once clients have migrated to declareModuleId
private static final Node GOOG_MODULE_DECLARNAMESPACE =
IR.getprop(GOOG_MODULE.cloneTree(), "declareNamespace");
private static final String TOGGLE_NAME_PREFIX = "TOGGLE_";
/**
* Map from module path to module. These modules represent files and thus will contain all goog
* namespaces that are in the file. These are not the same modules in modulesByGoogNamespace.
*/
private final Map modulesByPath = new HashMap<>();
/**
* Map from Closure namespace to module. These modules represent just the single namespace and
* thus each module has only one goog namespace in its {@link ModuleMetadata#googNamespaces()}.
* These are not the same modules in modulesByPath.
*/
private final Map modulesByGoogNamespace = new HashMap<>();
/** The current module being traversed. */
private ModuleMetadataBuilder currentModule;
/**
* The module currentModule is nested under, if any. Modules are expected to be at most two deep
* (a script and then a goog.loadModule call).
*/
private @Nullable ModuleMetadataBuilder parentModule;
/** The call to goog.loadModule we are traversing. */
private @Nullable Node loadModuleCall;
private final AbstractCompiler compiler;
private final boolean processCommonJsModules;
private final ResolutionMode moduleResolutionMode;
public GatherModuleMetadata(
AbstractCompiler compiler,
boolean processCommonJsModules,
ResolutionMode moduleResolutionMode) {
this.compiler = compiler;
this.processCommonJsModules = processCommonJsModules;
this.moduleResolutionMode = moduleResolutionMode;
}
private class ModuleMetadataBuilder {
private boolean ambiguous;
private boolean hasModuleBody;
private Node declaredModuleId;
private Node declaresLegacyNamespace;
final ModuleMetadata.Builder metadataBuilder;
final LinkedHashMultiset googNamespaces = LinkedHashMultiset.create();
ModuleMetadataBuilder(Node rootNode, @Nullable ModulePath path) {
this.metadataBuilder =
ModuleMetadata.builder()
.path(path)
.rootNode(rootNode)
.moduleType(ModuleType.SCRIPT)
.usesClosure(false)
.isTestOnly(false);
}
void moduleType(ModuleType type, NodeTraversal t, Node n) {
checkNotNull(type);
if (metadataBuilder.moduleType() == type) {
return;
}
if (metadataBuilder.moduleType() == ModuleType.SCRIPT) {
metadataBuilder.moduleType(type);
return;
}
ambiguous = true;
t.report(n, MIXED_MODULE_TYPE, metadataBuilder.moduleType().description, type.description);
}
void recordDeclareModuleId(Node declaredModuleId) {
this.declaredModuleId = declaredModuleId;
}
void recordDeclareLegacyNamespace(Node declaresLegacyNamespace) {
this.declaresLegacyNamespace = declaresLegacyNamespace;
}
boolean isScript() {
return metadataBuilder.moduleType() == ModuleType.SCRIPT;
}
ModuleMetadata build() {
metadataBuilder.googNamespacesBuilder().addAll(googNamespaces);
if (!ambiguous) {
if (hasModuleBody && metadataBuilder.moduleType() == ModuleType.SCRIPT) {
// A script with no imports or exports, but has a module body, must be an ES module.
metadataBuilder.moduleType(ModuleType.ES6_MODULE);
}
if (declaredModuleId != null && metadataBuilder.moduleType() != ModuleType.ES6_MODULE) {
compiler.report(JSError.make(declaredModuleId, DECLARE_MODULE_ID_OUTSIDE_ES6_MODULE));
}
if (declaresLegacyNamespace != null) {
if (metadataBuilder.moduleType() == ModuleType.GOOG_MODULE) {
metadataBuilder.moduleType(ModuleType.LEGACY_GOOG_MODULE);
} else {
compiler.report(
JSError.make(declaresLegacyNamespace, DECLARE_LEGACY_NAMESPACE_IN_NON_MODULE));
}
}
}
return metadataBuilder.build();
}
}
private static final QualifiedName GOOG_LOADMODULE = QualifiedName.of("goog.loadModule");
/** Traverses the AST and build a sets of {@link ModuleMetadata}s. */
private final class Finder implements NodeTraversal.Callback {
// Store both names and vars. Strings alone is insufficient to determine whether a name is
// actually a toggle module (since it could have been shadowed, or may have been defined in a
// different file), but looking up by only vars is much slower. This way we can do a fast name
// lookup, followed by a slower var lookup only if the name is known to be a toggle module name.
final Set toggleModuleNames = new HashSet<>();
final Set toggleModules = new HashSet<>();
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case SCRIPT:
enterModule(t, n, t.getInput().getPath());
break;
case IMPORT:
case EXPORT:
visitImportOrExport(t, n);
break;
case CALL:
if (n.isCall() && GOOG_LOADMODULE.matches(n.getFirstChild())) {
loadModuleCall = n;
enterModule(t, n, null);
}
break;
case MODULE_BODY:
currentModule.hasModuleBody = true;
break;
case DYNAMIC_IMPORT:
visitDynamicImport(n);
break;
default:
break;
}
return true;
}
private void visitImportOrExport(NodeTraversal t, Node importOrExport) {
checkNotNull(currentModule);
currentModule.moduleType(ModuleType.ES6_MODULE, t, importOrExport);
if (importOrExport.isImport()
// export from
|| (importOrExport.hasTwoChildren() && importOrExport.getLastChild().isStringLit())) {
currentModule
.metadataBuilder
.es6ImportSpecifiersBuilder()
.add(importOrExport.getLastChild().getString());
}
}
private void visitDynamicImport(Node dynamicImport) {
if (dynamicImport.getFirstChild().isStringLit()) {
currentModule
.metadataBuilder
.es6ImportSpecifiersBuilder()
.add(dynamicImport.getFirstChild().getString());
}
}
private void enterModule(NodeTraversal t, Node n, @Nullable ModulePath path) {
ModuleMetadataBuilder newModule = new ModuleMetadataBuilder(n, path);
if (currentModule != null) {
if (parentModule != null) {
t.report(n, INVALID_NESTED_LOAD_MODULE);
}
parentModule = currentModule;
}
currentModule = newModule;
}
private void leaveModule() {
checkNotNull(currentModule);
ModuleMetadata module = currentModule.build();
if (module.path() != null) {
modulesByPath.put(module.path().toString(), module);
}
for (String namespace : module.googNamespaces()) {
modulesByGoogNamespace.put(namespace, module);
}
if (parentModule != null) {
parentModule.metadataBuilder.nestedModulesBuilder().add(module);
}
currentModule = parentModule;
parentModule = null;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (processCommonJsModules && currentModule != null && currentModule.isScript()) {
// A common JS import (call to "require") does not force a module to be rewritten as
// commonJS. Only an export statement.
if (ProcessCommonJSModules.isCommonJsExport(t, n, moduleResolutionMode)) {
currentModule.moduleType(ModuleType.COMMON_JS, t, n);
return;
}
}
switch (n.getToken()) {
case SCRIPT:
leaveModule();
break;
case NAME:
visitName(t, n);
break;
case CALL:
if (loadModuleCall == n) {
leaveModule();
loadModuleCall = null;
} else {
visitGoogCall(t, n);
}
break;
default:
break;
}
}
private boolean isFromGoogImport(Var goog) {
Node nameNode = goog.getNameNode();
// Because other tools are regex based we force importing this file as "import * as goog".
return nameNode != null
&& nameNode.isImportStar()
&& nameNode.getString().equals("goog")
&& nameNode.getParent().getFirstChild().isEmpty()
&& nameNode.getParent().getLastChild().getString().endsWith("/goog.js");
}
private void visitName(NodeTraversal t, Node n) {
String name = n.getString();
if (toggleModuleNames.contains(name)) {
Var nameVar = t.getScope().getVar(name);
if (toggleModules.contains(nameVar)) {
Node parent = n.getParent();
if (parent.isGetProp()) {
addToggle(t, n, parent.getString());
} else if (!NodeUtil.isNameDeclaration(parent)) {
t.report(
n,
INVALID_TOGGLE_USAGE,
"toggle modules may not be used other than looking up properties");
}
}
}
if (!"goog".equals(name)) {
return;
}
Var root = t.getScope().getVar("goog");
if (root != null && !isFromGoogImport(root)) {
return;
}
currentModule.metadataBuilder.usesClosure(true);
}
private void visitGoogCall(NodeTraversal t, Node n) {
if (!n.hasChildren()
|| !n.getFirstChild().isGetProp()
|| !n.getFirstChild().isQualifiedName()) {
return;
}
Node getprop = n.getFirstChild();
Node firstProp = n.getFirstChild();
while (firstProp.isGetProp()) {
firstProp = firstProp.getFirstChild();
}
if (!firstProp.isName() || !firstProp.getString().equals("goog")) {
return;
}
Var root = t.getScope().getVar("goog");
// If this is a locally defined variable it can't be the global "goog", so exit early.
if (root != null && root.isLocal() && !root.getScope().isModuleScope()) {
return;
}
// If this is a module-level variable but wasn't created by importing goog.js, return.
if (root != null && root.getScope().isModuleScope() && !isFromGoogImport(root)) {
return;
}
// If goog is defined in this script then it does not use Closure. If this is a bundle with
// base.js in it, then it doesn't need base.js again.
if (root == null
|| NodeUtil.getEnclosingScript(root.getNameNode()) != NodeUtil.getEnclosingScript(n)) {
currentModule.metadataBuilder.usesClosure(true);
}
if (getprop.matchesQualifiedName(GOOG_PROVIDE)) {
currentModule.moduleType(ModuleType.GOOG_PROVIDE, t, n);
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
String namespace = n.getLastChild().getString();
addNamespace(currentModule, ModuleType.GOOG_PROVIDE, namespace, t, n);
} else {
t.report(n, ClosureRewriteModule.INVALID_PROVIDE_NAMESPACE);
currentModule.metadataBuilder.usesClosure(false);
}
} else if (getprop.matchesQualifiedName(GOOG_MODULE)) {
currentModule.moduleType(ModuleType.GOOG_MODULE, t, n);
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
String namespace = n.getLastChild().getString();
addNamespace(currentModule, ModuleType.GOOG_MODULE, namespace, t, n);
} else {
t.report(n, ClosureRewriteModule.INVALID_MODULE_ID_ARG);
currentModule.metadataBuilder.usesClosure(false);
}
} else if (getprop.matchesQualifiedName(GOOG_MODULE_GET)) {
// Look for a module named ???$2etoggles and a getprop of .TOGGLE_??? on the call's return
if (!n.hasTwoChildren()
|| !n.getLastChild().isStringLit()
|| !n.getLastChild().getString().endsWith("$2etoggles")) {
return; // only do anything with toggle namespaces
}
Node parent = n.getParent();
if (!parent.isGetProp()) {
t.report(
n,
INVALID_TOGGLE_USAGE,
"goog.module.get of toggles module must immediately look up a single toggle");
return;
}
addToggle(t, parent, parent.getString());
} else if (getprop.matchesQualifiedName(GOOG_MODULE_DECLARELEGACYNAMESPACE)) {
currentModule.recordDeclareLegacyNamespace(n);
} else if (getprop.matchesQualifiedName(GOOG_DECLARE_MODULE_ID)
|| getprop.matchesQualifiedName(GOOG_MODULE_DECLARNAMESPACE)) {
if (currentModule.declaredModuleId != null) {
t.report(n, MULTIPLE_DECLARE_MODULE_NAMESPACE);
}
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
currentModule.recordDeclareModuleId(n);
String namespace = n.getLastChild().getString();
addNamespace(currentModule, ModuleType.GOOG_MODULE, namespace, t, n);
} else {
t.report(n, INVALID_DECLARE_MODULE_ID_CALL);
}
} else if (getprop.matchesQualifiedName(GOOG_REQUIRE)) {
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
String namespace = n.getLastChild().getString();
currentModule.metadataBuilder.stronglyRequiredGoogNamespacesBuilder().add(namespace);
if (namespace.endsWith("$2etoggles")) {
// Track imports of *.toggles.ts, which are rewritten to $2etoggles.
Node callParent = n.getParent();
Node lhs = callParent.getFirstChild();
if (callParent.isDestructuringLhs()) {
// const {TOGGLE_foo} = goog.require('foo$2etoggles');
for (Node key : lhs.children()) {
if (key.isStringKey()) {
addToggle(t, n, key.getString());
} else {
t.report(n, INVALID_TOGGLE_USAGE, "must be destructured with string keys");
}
}
} else if (callParent.isName()) {
// const fooToggles = goog.require('foo$2etoggles');
String name = callParent.getString();
Var nameVar = t.getScope().getVar(name);
toggleModules.add(nameVar);
toggleModuleNames.add(name);
} else if (currentModule.metadataBuilder.moduleType() != ModuleType.GOOG_PROVIDE) {
// Side-effect toggle-module imports are not allowed, since they don't actually do
// anything. We allow it in `goog.provide()` files because there's no other way to
// import, and since toggle modules don't declare a legacy namespace, it's unusable
// without a `goog.module.get()` (so we can catch the toggle use there, instead).
t.report(n, INVALID_TOGGLE_USAGE, "import must be assigned");
}
}
} else {
t.report(n, INVALID_REQUIRE_NAMESPACE);
}
} else if (getprop.matchesQualifiedName(GOOG_REQUIRE_TYPE)) {
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
currentModule
.metadataBuilder
.weaklyRequiredGoogNamespacesBuilder()
.add(n.getLastChild().getString());
} else {
t.report(n, INVALID_REQUIRE_TYPE);
}
} else if (getprop.matchesQualifiedName(GOOG_MAYBE_REQUIRE)) {
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
currentModule
.metadataBuilder
.maybeRequiredGoogNamespacesBuilder()
.add(n.getLastChild().getString());
} else {
t.report(n, INVALID_MAYBE_REQUIRE);
}
} else if (getprop.matchesQualifiedName(GOOG_SET_TEST_ONLY)) {
if (n.hasOneChild() || (n.hasTwoChildren() && n.getLastChild().isStringLit())) {
currentModule.metadataBuilder.isTestOnly(true);
} else {
t.report(n, INVALID_SET_TEST_ONLY);
}
} else if (getprop.matchesQualifiedName(GOOG_REQUIRE_DYNAMIC)) {
if (n.hasTwoChildren() && n.getLastChild().isStringLit()) {
currentModule
.metadataBuilder
.dynamicallyRequiredGoogNamespacesBuilder()
.add(n.getLastChild().getString());
} else {
t.report(n, INVALID_REQUIRE_DYNAMIC);
}
}
}
/** Record a toggle usage (either a destructured import or a property lookup on a module). */
private void addToggle(NodeTraversal t, Node n, String name) {
if (name.startsWith(TOGGLE_NAME_PREFIX)) {
String toggleName = name.substring(TOGGLE_NAME_PREFIX.length());
currentModule.metadataBuilder.readTogglesBuilder().add(toggleName);
} else {
t.report(n, INVALID_TOGGLE_USAGE, "all toggle names must start with `TOGGLE_`");
}
}
/**
* Adds the namespaces to the module and checks if the given Closure namespace is a duplicate or
* not.
*/
private void addNamespace(
ModuleMetadataBuilder module,
ModuleType moduleType,
String namespace,
NodeTraversal t,
Node n) {
if (moduleType.equals(ModuleType.GOOG_PROVIDE)
|| moduleType.equals(ModuleType.LEGACY_GOOG_MODULE)) {
if (!NodeUtil.isValidQualifiedName(
compiler.getOptions().getLanguageIn().toFeatureSet(), namespace)) {
compiler.report(JSError.make(n, INVALID_NAMESPACE_OR_MODULE_ID, namespace));
}
}
if (moduleType.equals(ModuleType.GOOG_MODULE)
|| moduleType.equals(ModuleType.LEGACY_GOOG_MODULE)) {
// non-legacy goog.modules don't technically need to be valid qualified names
if (!isValidModuleId(namespace)) {
compiler.report(JSError.make(n, INVALID_NAMESPACE_OR_MODULE_ID, namespace));
}
}
ModuleType existingType = null;
String existingFileSource = null;
if (module.googNamespaces.contains(namespace)) {
existingType = module.metadataBuilder.moduleType();
existingFileSource = t.getSourceName();
} else {
ModuleMetadata existingModule = modulesByGoogNamespace.get(namespace);
if (existingModule != null) {
existingType = existingModule.moduleType();
existingFileSource = existingModule.rootNode().getSourceFileName();
}
}
currentModule.googNamespaces.add(namespace);
if (existingType != null) {
switch (existingType) {
case ES6_MODULE:
case GOOG_MODULE:
case LEGACY_GOOG_MODULE:
{
DiagnosticType diagnostic =
moduleType.equals(ModuleType.GOOG_PROVIDE)
? ClosurePrimitiveErrors.DUPLICATE_NAMESPACE_AND_MODULE
: ClosurePrimitiveErrors.DUPLICATE_MODULE;
t.report(n, diagnostic, namespace, existingFileSource);
return;
}
case GOOG_PROVIDE:
{
DiagnosticType diagnostic =
moduleType.equals(ModuleType.GOOG_PROVIDE)
? ClosurePrimitiveErrors.DUPLICATE_NAMESPACE
: ClosurePrimitiveErrors.DUPLICATE_NAMESPACE_AND_MODULE;
t.report(n, diagnostic, namespace, existingFileSource);
return;
}
case COMMON_JS:
case SCRIPT:
// Fall through, error
}
throw new IllegalStateException("Unexpected module type: " + existingType);
}
}
}
@Override
public void process(Node externs, Node root) {
NodeTraversal.traverse(compiler, externs, new Finder());
NodeTraversal.traverse(compiler, root, new Finder());
compiler.setModuleMetadataMap(new ModuleMetadataMap(modulesByPath, modulesByGoogNamespace));
}
// Must match closure/base.js's goog.VALID_MODULE_RE_ & also validates that dotted segments are
// non-empty.
private static boolean isValidModuleId(String id) {
for (String segment : DOT_SPLITTER.split(id)) {
if (segment.isEmpty()) {
return false;
}
for (int i = 0; i < segment.length(); i++) {
if (!Identifiers.isIdentifierPart(segment.charAt(i))) {
return false;
}
}
}
return NAMESPACE_SEGMENT_REGEX.matcher(id).matches();
}
private static final Pattern NAMESPACE_SEGMENT_REGEX =
Pattern.compile("^[a-zA-Z_$][a-zA-Z0-9_$.]*$");
private static final Splitter DOT_SPLITTER = Splitter.on('.');
}