diff --git a/platform/lang-api/src/com/intellij/formatting/fileSet/FileSetEntry.java b/platform/lang-api/src/com/intellij/formatting/fileSet/FileSetEntry.java
new file mode 100644
index 000000000000..28205246ae37
--- /dev/null
+++ b/platform/lang-api/src/com/intellij/formatting/fileSet/FileSetEntry.java
@@ -0,0 +1,139 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.formatting.fileSet;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.regex.Pattern;
+
+/**
+ * Describes a set of files specified by a directory and/or by a file mask.
+ *
+ * Examples:
+ *
+ * -
+ * {@code *.test}
+ * A file ending with .test in any directory
+ *
+ * -
+ * {@code /foo/*.test}
+ * A file with *.test extension immediately under project-dir/foo directory.
+ *
+ * -
+ * {@code foo/*.test}
+ * A file with .test extension if their parent directory is "foo", equivalent to /**/foo/*.test
+ *
+ * -
+ * {@code /*.test}
+ * A file with .test extension directly under project root directory.
+ *
+ *
+ */
+public class FileSetEntry {
+
+ private @Nullable Pattern myPathPattern;
+ private @Nullable Pattern myFileNamePattern;
+
+ public FileSetEntry(@NotNull String spec) {
+ compileSpec(spec);
+ }
+
+ private void compileSpec(@NotNull String spec) {
+ String pathSpec = "";
+ String fileSpec;
+ int lastSlashPos = spec.lastIndexOf('/');
+ if (lastSlashPos >= 0) {
+ fileSpec = spec.substring(lastSlashPos + 1);
+ pathSpec = spec.substring(0, lastSlashPos + 1);
+ if (pathSpec.length() > 0 && pathSpec.charAt(0) != '/') {
+ pathSpec = "/**/" + pathSpec;
+ }
+ }
+ else {
+ fileSpec = spec;
+ }
+ if (!pathSpec.isEmpty()) {
+ myPathPattern = Pattern.compile(specToRegexp(pathSpec, true));
+ }
+ if (!fileSpec.isEmpty()) {
+ myFileNamePattern = Pattern.compile(specToRegexp(fileSpec, false));
+ }
+ }
+
+ private static String specToRegexp(@NotNull String spec, boolean isPathSpec) {
+ StringBuilder sb = new StringBuilder();
+ char[] chars = spec.toCharArray();
+ int i = 0;
+ while (i < chars.length) {
+ char c = chars[i];
+ switch (c) {
+ case '*':
+ if (isPathSpec && i < chars.length - 1 && chars[i + 1] == '*') {
+ sb.append("([^/]*/)*");
+ i++;
+ if (i < chars.length - 1 && chars[i + 1] == '/') {
+ i ++;
+ }
+ }
+ else {
+ sb.append("[^/]*");
+ }
+ break;
+ case '?':
+ sb.append("[^/]");
+ break;
+ default:
+ if (isRegexSpecialChar(c)) {
+ sb.append('\\').append(c);
+ }
+ else {
+ sb.append(c);
+ }
+ }
+ i ++;
+ }
+ return sb.toString();
+ }
+
+
+ private static boolean isRegexSpecialChar(char c) {
+ return "^${}[]().*+-&".indexOf(c) >= 0;
+ }
+
+ /**
+ * Checks if the file set entry matches the given virtual file.
+ *
+ * @param project The current project.
+ * @param virtualFile The virtual file to check against.
+ * @return True if there is a match, false otherwise.
+ */
+ public boolean matches(@NotNull Project project, @NotNull VirtualFile virtualFile) {
+ if (myFileNamePattern == null && myPathPattern == null) return false; // Empty spec matches nothing
+ String name = virtualFile.getName();
+ VirtualFile parent = virtualFile.getParent();
+ String path = getRelativePath(project, parent) + "/";
+ return patternMatches(myPathPattern, path) && patternMatches(myFileNamePattern, name);
+ }
+
+ @NotNull
+ private static String getRelativePath(@NotNull Project project, @Nullable VirtualFile parent) {
+ VirtualFile projectDir = project.getBaseDir();
+ String projectPath = projectDir.getPath();
+ if (parent != null) {
+ String parentPath = parent.getPath();
+ if (parentPath.startsWith(projectPath)) {
+ return parentPath.substring(projectPath.length());
+ }
+ else {
+ return parentPath;
+ }
+ }
+ return "";
+ }
+
+ private static boolean patternMatches(@Nullable Pattern pattern, @NotNull String str) {
+ return pattern == null || pattern.matcher(str).matches();
+ }
+}
diff --git a/platform/platform-tests/testSrc/com/intellij/formatting/fileSet/FileSetEntryTest.java b/platform/platform-tests/testSrc/com/intellij/formatting/fileSet/FileSetEntryTest.java
new file mode 100644
index 000000000000..51f5014ca113
--- /dev/null
+++ b/platform/platform-tests/testSrc/com/intellij/formatting/fileSet/FileSetEntryTest.java
@@ -0,0 +1,66 @@
+// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
+package com.intellij.formatting.fileSet;
+
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.PlatformTestCase;
+import org.jetbrains.annotations.NotNull;
+
+public class FileSetEntryTest extends PlatformTestCase {
+
+ public void testSimpleFileMask() {
+ FileSetEntry entry = new FileSetEntry("*.java");
+ assertTrue(entry.matches(getProject(), createFile("src/test.java")));
+ assertFalse(entry.matches(getProject(), createFile("src/test.txt")));
+ assertTrue(entry.matches(getProject(), createFile("file3.java")));
+ }
+
+ public void testFileMaskWithDir() {
+ FileSetEntry entry = new FileSetEntry("/src/*.java");
+ assertTrue(entry.matches(getProject(), createFile("src/test.java")));
+ assertFalse(entry.matches(getProject(), createFile("src/test.txt")));
+ assertFalse(entry.matches(getProject(), createFile("file3.java")));
+ }
+
+ public void testExtendedDirSpec() {
+ FileSetEntry entry = new FileSetEntry("/src/**/test/*.java");
+ assertTrue(entry.matches(getProject(), createFile("src/test/file1.java")));
+ assertTrue(entry.matches(getProject(), createFile("src/dir1/test/file1.java")));
+ assertTrue(entry.matches(getProject(), createFile("src/dir1/dir2/test/file1.java")));
+ assertFalse(entry.matches(getProject(), createFile("src/dir1/dir2/test/dir3/file1.java")));
+ FileSetEntry entry2 = new FileSetEntry("/src/**/test/**/*.java");
+ assertTrue(entry2.matches(getProject(), createFile("src/test/a/file1.java")));
+ assertTrue(entry2.matches(getProject(), createFile("src/b/test/a/file1.java")));
+ }
+
+ public void testAnyCharMarks() {
+ FileSetEntry entry = new FileSetEntry("/test-??/**/test?.java");
+ assertFalse(entry.matches(getProject(), createFile("test-1/test2.java")));
+ assertTrue(entry.matches(getProject(), createFile("test-12/test3.java")));
+ assertTrue(entry.matches(getProject(), createFile("test-12/test/testa.java")));
+ }
+
+
+ public void testRelativeDirTest() {
+ FileSetEntry entry = new FileSetEntry("test/*.java");
+ assertTrue(entry.matches(getProject(), createFile("src/test/file1.java")));
+ assertTrue(entry.matches(getProject(), createFile("test/file2.java")));
+ assertTrue(entry.matches(getProject(), createFile("a/b/c/d/test/file3.java")));
+ }
+
+ private VirtualFile createFile(@NotNull String path) {
+ String[] dirNames = path.split("/");
+ VirtualFile baseDir = getProject().getBaseDir();
+ for (int i = 0; i < dirNames.length - 1; i ++) {
+ VirtualFile existing = VfsUtilCore.findRelativeFile(dirNames[i], baseDir);
+ if (existing == null) {
+ baseDir = createChildDirectory(baseDir, dirNames[i]);
+ }
+ else {
+ baseDir = existing;
+ }
+ }
+ return createChildData(baseDir, dirNames[dirNames.length - 1]);
+ }
+
+}